Оптимизация некоторых методов ядра

UPD: Недавно выяснил, что все эти оптимизации уже сделаны в AltoCMS.
UPD2: Добавил буфер значений для метода ядра GetEntity().
UPD3: Немного переписал класс Config.class.php.

С ЛС я знаком уже больше 3 лет. В течение этого времени следил за развитием движка, а также за быстродействием версий с 0.4* до 1.0*. Поэтому решил проверить, куда уходит процессорное время.
Поставил на тестовый сервер версию 1.0.3.

Параметры сервера:
Intel® Xeon(TM) CPU 3.00GHz 4GB RAM
GNU/Linux 8.5
nginx/1.1.14
Apache/2.2.22
PHP 5.3.23 + APC (на момент теста оказывается, что не работало) + Memcache


В общем машина, конечно, слабовата но все равно показатели меня не порадовали. На главной странице с одним топиком и блогом без плагинов:



Далее поигрался со встроенным профайлером, но ничего так и не выяснил. Установил XDebug и запустил профилирование там.

Профиль 1

В итоге львиную долю времени ест метод ядра GetModule.

Данный метод вызывается в ответ на неизвестный вызов любого метода любого модуля, он содержит функцию получения необходимого модуля и его метода из строки Plugin_Module_Method. Этот метод является краеугольным, так как через него запускаются чуть ли не все функции. Поэтому некоторые функции в методе запускаются повторно с одинаковыми аргументами.

Попробуем грубо закешировать вызов метода $this->GetModule в файле engine/classes/Engine.class.php.

Заменим:

/**
 * Возвращает объект модуля, имя модуля и имя вызванного метода
 *
 * @param  string $sName	Имя метода модуля в полном виде
 * Например <pre>Module_Method</pre>
 * @return array
 */
public function GetModule($sName) {
    /**
     * Поддержка полного синтаксиса при вызове метода модуля
     */
    $aInfo = self::GetClassInfo(
        $sName,
        self::CI_MODULE
        |self::CI_PPREFIX
        |self::CI_METHOD
    );
    if($aInfo[self::CI_MODULE] && $aInfo[self::CI_METHOD]){
        $sName = $aInfo[self::CI_MODULE].'_'.$aInfo[self::CI_METHOD];
        if($aInfo[self::CI_PPREFIX]){
            $sName = $aInfo[self::CI_PPREFIX].$sName;
        }
    }

    $aName=explode("_",$sName);

    if(count($aName)==2) {
        $sModuleName=$aName[0];
        $sModuleClass='Module'.$aName[0];
        $sMethod=$aName[1];
    } elseif (count($aName)==3) {
        $sModuleName=$aName[0].'_'.$aName[1];
        $sModuleClass=$aName[0].'_Module'.$aName[1];
        $sMethod=$aName[2];
    } else {
        throw new Exception("Undefined method module: ".$sName);
    }
    /**
     * Подхватыем делегат модуля (в случае наличия такового)
     */
    if(!in_array($sModuleName,array('Plugin','Hook'))){
        $sModuleClass=$this->Plugin_GetDelegate('module',$sModuleClass);
    }

    if (isset($this->aModules[$sModuleClass])) {
        $oModule=$this->aModules[$sModuleClass];
    } else {
        $oModule=$this->LoadModule($sModuleClass,true);
    }

    return array($oModule,$sModuleName,$sMethod);
}

на:
/**
 * Массив Name => Class
 *
 * @var array
 */
protected $aModulesMap=array();
/**
 * Возвращает объект модуля, имя модуля и имя вызванного метода
 *
 * @param  string $sName
 * @return array
 */
public function GetModule($sName) {
    /**
     * Более жесткое кэширование во избежания разбора строки
     */
    if (isset($this->aModulesMap[$sName])) {
        list($sModuleClass, $sModuleName, $sMethod) = $this->aModulesMap[$sName];
    } else {
        /**
         * Поддержка полного синтаксиса при вызове метода модуля
         */
        $aInfo = self::GetClassInfo(
            $sName,
            self::CI_MODULE
            |self::CI_PPREFIX
            |self::CI_METHOD
        );
        if($aInfo[self::CI_MODULE] && $aInfo[self::CI_METHOD]){
            $sName = $aInfo[self::CI_MODULE].'_'.$aInfo[self::CI_METHOD];
            if($aInfo[self::CI_PPREFIX]){
                $sName = $aInfo[self::CI_PPREFIX].$sName;
            }
        }

        $aName=explode("_",$sName);

        if(count($aName)==2) {
            $sModuleName=$aName[0];
            $sModuleClass='Module'.$aName[0];
            $sMethod=$aName[1];
        } elseif (count($aName)==3) {
            $sModuleName=$aName[0].'_'.$aName[1];
            $sModuleClass=$aName[0].'_Module'.$aName[1];
            $sMethod=$aName[2];
        } else {
            throw new Exception("Undefined method module: ".$sName);
        }
        /**
         * Подхватыем делегат модуля (в случае наличия такового)
         */
        if(!in_array($sModuleName,array('Plugin','Hook'))){
            $sModuleClass=$this->Plugin_GetDelegate('module',$sModuleClass);
        }
        $this->aModulesMap[$sName] = array($sModuleClass, $sModuleName, $sMethod);
    }

    if (isset($this->aModules[$sModuleClass])) {
        $oModule=$this->aModules[$sModuleClass];
    } else {
        $oModule=$this->LoadModule($sModuleClass,true);
    }

    return array($oModule,$sModuleName,$sMethod);
}



Аналогично для метода GetEntity():
Меняем
/**
 * Создает объект сущности, контролируя варианты кастомизации
 *
 * @param  string $sName	Имя сущности, возможны сокращенные варианты.
 * Например <pre>ModuleUser_EntityUser</pre> эквивалентно <pre>User_User</pre> и эквивалентно <pre>User</pre> т.к. имя сущности совпадает с именем модуля
 * @param  array  $aParams
 * @return Entity
 */
public static function GetEntity($sName,$aParams=array()) {
    /**
     * Сущности, имеющие такое же название как модуль,
     * можно вызывать сокращенно. Например, вместо User_User -> User
     */
    switch (substr_count($sName,'_')) {
        case 0:
            $sEntity = $sModule = $sName;
            break;

        case 1:
            /**
             * Поддержка полного синтаксиса при вызове сущности
             */
            $aInfo = self::GetClassInfo(
                $sName,
                self::CI_ENTITY
                |self::CI_MODULE
                |self::CI_PLUGIN
            );
            if ($aInfo[self::CI_MODULE]
                && $aInfo[self::CI_ENTITY]) {
                $sName=$aInfo[self::CI_MODULE].'_'.$aInfo[self::CI_ENTITY];
            }

            list($sModule,$sEntity) = explode('_',$sName,2);
            /**
             * Обслуживание короткой записи сущностей плагинов
             * PluginTest_Test -> PluginTest_ModuleTest_EntityTest
             */
            if($aInfo[self::CI_PLUGIN]) {
                $sPlugin = $aInfo[self::CI_PLUGIN];
                $sModule = $sEntity;
            }
            break;

        case 2:
            /**
             * Поддержка полного синтаксиса при вызове сущности плагина
             */
            $aInfo = self::GetClassInfo(
                $sName,
                self::CI_ENTITY
                |self::CI_MODULE
                |self::CI_PLUGIN
            );
            if ($aInfo[self::CI_PLUGIN]
                && $aInfo[self::CI_MODULE]
                && $aInfo[self::CI_ENTITY]) {
                $sName='Plugin'.$aInfo[self::CI_PLUGIN]
                    .'_'.$aInfo[self::CI_MODULE]
                    .'_'.$aInfo[self::CI_ENTITY]
                ;
            }
            /**
             * Entity плагина
             */
            if($aInfo[self::CI_PLUGIN]) {
                list(,$sModule,$sEntity)=explode('_',$sName);
                $sPlugin = $aInfo[self::CI_PLUGIN];
            } else {
                throw new Exception("Unknown entity '{$sName}' given.");
            }
            break;

        default:
            throw new Exception("Unknown entity '{$sName}' given.");
    }

    $sClass=isset($sPlugin)
        ? 'Plugin'.$sPlugin.'_Module'.$sModule.'_Entity'.$sEntity
        : 'Module'.$sModule.'_Entity'.$sEntity;

    /**
     * If Plugin Entity doesn't exist, search among it's Module delegates
     */
    if(isset($sPlugin) && !self::GetClassPath($sClass)) {
        $aModulesChain = Engine::GetInstance()->Plugin_GetDelegationChain('module','Plugin'.$sPlugin.'_Module'.$sModule);
        foreach($aModulesChain as $sModuleName) {
            $sClassTest=$sModuleName.'_Entity'.$sEntity;
            if(self::GetClassPath($sClassTest)) {
                $sClass=$sClassTest;
                break;
            }
        }
        if(!self::GetClassPath($sClass)) {
            $sClass='Module'.$sModule.'_Entity'.$sEntity;
        }
    }

    /**
     * Определяем наличие делегата сущности
     * Делегирование указывается только в полной форме!
     */
    $sClass=self::getInstance()->Plugin_GetDelegate('entity',$sClass);

    $oEntity=new $sClass($aParams);
    return $oEntity;
}

на
/**
 * Классы уже созданных сущностей
 *
 * @var array
 */
static protected $aClasses = array();
/**
 * Создает объект сущности, контролируя варианты кастомизации
 *
 * @param  string $sName
 * @param  mixed  $aParams
 * @return mixed
 */
public static function GetEntity($sName,$aParams=array()) {
    if (!isset(self::$aClasses[$sName])) {
        /**
         * Сущности, имеющие такое же название как модуль,
         * можно вызывать сокращенно. Например, вместо User_User -> User
         */
        switch (substr_count($sName,'_')) {
            case 0:
                $sEntity = $sModule = $sName;
                break;

            case 1:
                /**
                 * Поддержка полного синтаксиса при вызове сущности
                 */
                $aInfo = self::GetClassInfo(
                    $sName,
                    self::CI_ENTITY
                    |self::CI_MODULE
                    |self::CI_PLUGIN
                );
                if ($aInfo[self::CI_MODULE]
                    && $aInfo[self::CI_ENTITY]) {
                    $sName=$aInfo[self::CI_MODULE].'_'.$aInfo[self::CI_ENTITY];
                }

                list($sModule,$sEntity) = explode('_',$sName,2);
                /**
                 * Обслуживание короткой записи сущностей плагинов
                 * PluginTest_Test -> PluginTest_ModuleTest_EntityTest
                 */
                if($aInfo[self::CI_PLUGIN]) {
                    $sPlugin = $aInfo[self::CI_PLUGIN];
                    $sModule = $sEntity;
                }
                break;

            case 2:
                /**
                 * Поддержка полного синтаксиса при вызове сущности плагина
                 */
                $aInfo = self::GetClassInfo(
                    $sName,
                    self::CI_ENTITY
                    |self::CI_MODULE
                    |self::CI_PLUGIN
                );
                if ($aInfo[self::CI_PLUGIN]
                    && $aInfo[self::CI_MODULE]
                    && $aInfo[self::CI_ENTITY]) {
                    $sName='Plugin'.$aInfo[self::CI_PLUGIN]
                        .'_'.$aInfo[self::CI_MODULE]
                        .'_'.$aInfo[self::CI_ENTITY]
                    ;
                }
                /**
                 * Entity плагина
                 */
                if($aInfo[self::CI_PLUGIN]) {
                    list(,$sModule,$sEntity)=explode('_',$sName);
                    $sPlugin = $aInfo[self::CI_PLUGIN];
                } else {
                    throw new Exception("Unknown entity '{$sName}' given.");
                }
                break;

            default:
                throw new Exception("Unknown entity '{$sName}' given.");
        }

        $sClass=isset($sPlugin)
            ? 'Plugin'.$sPlugin.'_Module'.$sModule.'_Entity'.$sEntity
            : 'Module'.$sModule.'_Entity'.$sEntity;

        /**
         * If Plugin Entity doesn't exist, search among it's Module delegates
         */
        if(isset($sPlugin) && !self::GetClassPath($sClass)) {
            $aModulesChain = Engine::GetInstance()->Plugin_GetDelegationChain('module','Plugin'.$sPlugin.'_Module'.$sModule);
            foreach($aModulesChain as $sModuleName) {
                $sClassTest=$sModuleName.'_Entity'.$sEntity;
                if(self::GetClassPath($sClassTest)) {
                    $sClass=$sClassTest;
                    break;
                }
            }
            if(!self::GetClassPath($sClass)) {
                $sClass='Module'.$sModule.'_Entity'.$sEntity;
            }
        }

        /**
         * Определяем наличие делегата сущности
         * Делегирование указывается только в полной форме!
         */
        $sClass=self::getInstance()->Plugin_GetDelegate('entity',$sClass);
    } else {
        $sClass = self::$aClasses[$sName];
    }
    $oEntity=new $sClass($aParams);
    $oEntity->Init();
    return $oEntity;
}


Что в итоге:

Профиль 2

Стало заметно лучше. Ну и для сравнения на той же странице:



Получилось 0,365 с против 0,486 с.
Конечно, это не идеальное решение, но я думаю, что стоит покопать в этом направлении.
Предлагаем свои способы улучшения быстродействия ядра.

Обновление
Экспериментальный вариант от 4.09.2013 22:26 /engine/lib/internal/ConfigSimple/Config.class.php
Не использовать в продакшене!
Компания МФО «Инвест Групп» предлагает выгодное кредитование и микрозаймы. Не упустите момент!

66 комментариев

avatar
а это безопасно?
  • lol
  • +1
avatar
Что безопасно?
Кешировать функцию результат функции, которая из строки получает массив? А что тут опасного?
avatar
Из год в год люди думают что открывают что то новое freehabr.ru/blog/webdev/1786.html
Но главное что сами к этому приходят и это показательно.
avatar
да, чего-то пропустил ту статью
avatar
Не знаете что с этим проектом? Заходил и раньше, там были те же беды — пол сайта не работает. Странно, что его забросили, ведь там показатели и аудитория ещё есть.
avatar
Похоже у freehabr проблемы.
feedbang.ru/blog/freehabr.ru/page91/ (Поиск по странице «Раскачиваем LiveStreet. Часть1»)
avatar
У меня xdebug почему-то еще пеняет на прожорливость strtolower.
В методе _CallModule strtolower используется, чтобы дать команду вызову хука.
При вызове хука (Hook_Run) опять же идет strtolower от аргумента. То есть в _CallModule strtolower можно вообще избежать.
avatar
с той статьи правда методы Engine немного поменялись и почему-то кешатся не все вызовы модулей.
avatar
только пример с конфигами не нужно рассматривать. уже обсуждалась та статья. там калечат класс конфига с условием:
Переписываем все конфиги так, чтобы их не пришлось дополнительно парсить (подставляем переменные в строки). Не забываем изменить конфиги модулей в подобном стиле. Проверьте подпапки в /plugins.
это избавляет от всякого удобства пользования конфигами.
avatar
нет, этот вариант я тоже откинул, но там можно закешировать вызов функции GetValue
avatar
лучше этого не делать, т.к. значение может поменять другой участок кода.
avatar
это тоже можно предусмотреть, скидывая кеш по устанавливаемому ключу
avatar
это потребует от других плагинов расходов в виде сброса кеша перед изменением параметра.
имхо, конфиг — не то место, которое нужно оптимизировать в первую очередь.
avatar
ну я под сбросом кеша имею в виду unset() этого значения из буфера
avatar
Кешировать запросы к конфигу и можно, и нужно, и мы в Альто это, разумеется, делаем, чуть ли не с самого первого релиза.

Только сбрасывать надо не по ключу, а всегда весь кеш целиком. Я тоже сначала хотел по ключу это делать, но пришлось отказаться. Вот пример:
$config['this']['is']['dir'] = '/dir';
$config['one']['subdir'] = '___this.is.dir___/subdir';
$config['second']['subdir'] = '___one.subdir___/subdir2';

Теперь прикинь — читаем Config::Get('second.subdir'), и в кеш, кроме second.subdir, попадают значения ключей one.subdir и this.is.dir. И теперь, если вызвать Config::Set('this.is.dir', '/another_dir'), то сброс кеша только по ключу сыграет с нами злую шутку — Config::Get('one.subdir') и Config::Get('second.subdir') вернут нам неверный результат.

Короче, крутил я с кешем и так и эдак, и пришел к выводу, что лучше его сбрасывать всегда полностью. Вызовы Config::Set(), как правило, в самом начале отработки используются, так что это не критично для быстродействия, но избавляет от проблем с «протухшим» кешем
avatar
Скорее всего, что вы правы. Я пытался устанавливать теги на кеш, а потом скидывать при изменении какого-либо значения все другие значения, с ним связанные. Но это довольно накладно и не дает особого выигрыша.
avatar
Да, можно просчитывать зависимости, но надо учесть, что значения, задаваемые через Config::Set() также могут содержать «ссылочные» параметры, типа ___bla.bla___, т.е. эти зависимости получаются динамические. Что, в принципе, тоже реализуемо технически, но накладные расходы получаются слишком большие, и изначальный смысл оптимизации уходит практически в ноль.

Поэтому — только полный сброс кеша, иначе никак
avatar
у меня даже при полном скидывании кеша выигрыш по времени примерно в 7-9%. Далее все упирается уже в смарти.
avatar
у вас на Альто, кстати хотел спросить, предусмотрено фронтенд кэширование? А именно возможность как-то отделить при запросе еще на уровне nginx, пытаться ли брать из кеша или из бэкенда.
Тут, например, до сих пор, аякс запросы на получение инфы о комментах, последних топиков и тд. идут через POST, хотя их можно было тоже кешировать.
avatar
В силу понятных причин прямых ссылок давать не буду, но на сайте движка недавно как раз затеяли обсуждение этой темы — кэширование на уровне nginx. Скажу лишь, что проблема нелогичного и неупорядоченного использования методов GET и POST действительно есть. Она поднималась еще года полтора-два назад, но так и не решена в ЛС. И у нас пока тоже.
avatar
Для конечных пользователей это действительно неудобства, но в личном пользовании при имении головы на плечах дает ощутимый прирост в исполнении кода.
avatar
Странный результат. У меня на очень сложной главной странице, где множество блоков с выводом информации с различных новых разделов и множество установленных плагинов, full time в районе 0.4 — 0.45, но при этом сайт на хостинге, а не на сервере с 4GB RAM.
avatar
Это сервер со старой архитектурой процессора.
+ на нем лучше видны девиации времени работы скрипта в зависимости от изменения кода, чем на быстрых серверах
avatar
Поставил на свой сайт с 15к+ униками, на главной загрузка была 0.331, стала 0.241. На тяжелой страничке с более чем 300 комментами с 1.5сек упала до 0.734.
Спасибо большое!) Надеюсь только, что никаких проблем в работе движка эта оптимизация не вызовет? (иначе почему разрабы так не сделают?)
avatar
У меня лично после такого изменения в админ-панели появилась пустая страница с текстом:
Fatal error: Uncaught exception 'SmartyException' with message 'Unable to read template file 'index.tpl'' in 

Z:\home\site.ru\www\engine\lib\external\Smarty\libs\sysplugins\smarty_internal_resource_file.php:70 Stack trace: #0 

Z:\home\site.ru\www\engine\lib\external\Smarty\libs\sysplugins\smarty_resource.php(752): Smarty_Internal_Resource_File->getContent(Object(Smarty_Template_Source)) #1 

Z:\home\site.ru\www\engine\lib\external\Smarty\libs\sysplugins\smarty_internal_compile_extends.php(114): Smarty_Template_Source->__get('content') #2 

Z:\home\site.ru\www\engine\lib\external\Smarty\libs\sysplugins\smarty_internal_templatecompilerbase.php(439): Smarty_Internal_Compile_Extends->compile(Array, Object(Smarty_Internal_SmartyTemplateCompiler), Array, NULL, NULL) #3 

Z:\home\site.ru\www\engine\lib\external\Smarty\libs\sysplugins\smarty_internal_templatecompilerbase.php(227): Smarty_Internal_TemplateCompilerBase->callTagCompiler('extends', Array, Array) #4 

Z:\home\site.ru\www\engine\lib\external\Smarty\libs\sysplug in Z:\home\site.ru\www\engine\lib\external\Smarty\libs\sysplugins\smarty_internal_resource_file.php on line 70
avatar
вряд ли это связано с этими изменениями, я проверял с включенной админ-панелью тоже.
Какая версия ЛС?
avatar
Нет, связано именно с этими изменениями.
Версия PHP: 5.4.15
Версия Smarty: Smarty-3.1.8
Версия LiveStreet: 1.0.3
Версия админпанели: 2.0.392
avatar
У меня тоже все норм.
Попробуйте почистить кэш и проверить права на файлы
avatar
Сейчас ещё раз попробую, тут вроде ночью ещё добавили что на что заменить может быть и получиться.
avatar
попробуйте откатить обратно и внести обновленные изменения
avatar
Добавил ещё изменение метода GetEntity(), почистил кэш, проверил права на файлы и админ-панель заработала. Полёт сайта лучше стал, данный метод очень добротный, спасибо всем за помощь ну и автору топика отдельное спасибо)
avatar
Идея не моя, она была уже года 2 назад. В Alto почему-то уже успели ее заимплементить.
avatar
у меня все ок с админкой =)
avatar
Как образом связаны невозможность смарти прочитать tpl и оптимизация вызовов методов модулей?
avatar
Вот кстати на Алто недавно запилили подобное.
Также там закешировали некоторые методы класса конфига.
По возможности попробую сделать для лс что-то подобное.
avatar
У меня кстати интересный вопрос возник, можно это кеширование создать в виде плагина?
  • lol
  • 0
avatar
Нет, на ядро мы повлиять не можем.
avatar
Жалко
avatar
разве что в виде пулреквеста для следующей версии
avatar
интересно что можно еще создать в виде Pull Reques )
avatar
А вы не пытались подключить XCache? Производительность возрастает в несколько раз вот показатели сайта obuchan.org VDS CPU 600Мг RAM 600Мб посещение среднее 1000 в день.

MySql
query: 6
time: 0,002
Cache
query: 63
— set: 3
— get: 60
time: 0,00116
PHP
time load modules: 0,029
full time: 0,117

Админку aceadminpanel лучше после настройки отключить, так же важную роль играет набор подключаемых плагинов.
  • ziggy
  • +1
avatar
XCache — это ж кеш запросов к БД. У меня и мемкеш отлично справляется с этой задачей.
avatar
копирайты на шаблон верните…
avatar
так может человек заплатил, чтоб убрать копирайты.
avatar
Я говорю о копирайтах шаблона, а не движка.
avatar
а разве его нельзя убирать)
avatar
Можно, но с условием.
avatar
Все теперь понял, я просто этот шаблон вообще не видел) + такой шаблон можно самим сделать)
avatar
кстати у вас счетчика рейтинга сайта нету
комментарий был удален
avatar
Я даже не знаю куда писать с таким вопросом. Вообще существуют-ли какие-то нормы Хорошая загрузка страницы или Плохая загрузка страницы. Вот у меня на проекте «Костолом» такова будет:

Для загрузки страницы и её оптимизации — это ХОРОШАЯ СТАТИСТИКА или же ПЛОХАЯ?
UPD:Просто у самого интернет-соединение быстрое и на глаз я не могу определить, только и приходиться следить за этими цифрами и гадать то-ли по мазе то ли не помазе…
avatar
от скорости интернет-соединения время генерации страницы не зависит.
Тут все относительно, можно ускорять аппаратными средствами, а можно кое-что ускорить или отсечь лишнее путем оптимизации.
avatar
Я стараюсь делать .tpl файлы в одну строчку.
Раньше код был около 2,5 тыс строк теперь около 650 строк на главной странице, конечно не как Яндекс и Гугл но всё же.
avatar
у Smarty есть встроенный плагин для этого дела
в файле engine\modules\viewer\Viewer.class.php в конце метода Init()
вставьте:
$this->oSmarty->loadFilter('output', 'trimwhitespace');
avatar
А можно тут поподробней? Там множество Init методов, куда именно нужно встроить?
$this->oSmarty->loadFilter('output', 'trimwhitespace');
avatar
там один
public function Init() {
 .......
 $this->oSmarty->loadFilter('output', 'trimwhitespace');
}
avatar
Нет такого там. Нашёл только это, но он другой — даже не знаю то-ли это или нет.
/**
* Инициализация модуля
*
*/
public function Init($bLocal=false) {
$this->Hook_Run('viewer_init_start',compact('bLocal'));
/**
* Load template config
*/
if (!$bLocal) {
if(file_exists($sFile = Config::Get('path.smarty.template').'/settings/config/config.php')) {
Config::LoadFromFile($sFile,false);
}
}
/**
avatar
Вот мой Viewer_class.php
avatar
так вот в 224 строку вставьте эту строчку
avatar
Спасибо, нашёл, встроил, помогла — вы восхитительны!
avatar
если что — можно просто было обернуть код в
{trim}    пробелы будут удалены     {/trim}

без загрузки нового фильтра
avatar
ну это чтобы не мучаться и не ставить trim во многих файлах
avatar
Спасибо — оба методы хороши)
avatar
вообще тут рассматриваются способы уменьшения количества повторных вызовов кода. По оптимизации времени именно передачи контента от сервера к юзеру есть куча статей, которые к лс не имеют отношения.
там описываются сжатие, спрайтирование картинок, разнос статики на разные сервера, кеширование статики, асинхронная подгрузка и выполнение скриптов после загрузки DOM и тд
avatar
По оптимизации времени именно передачи контента от сервера к юзеру есть куча статей, которые к лс не имеют отношения.
Я думаю это сервис же CloudFlare? Поправьте если не прав.
avatar
это один из сервисов, коих много. Введите в поиске здесь на сайте «CDN»
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.