Способы оптимизации скорости загрузки страниц 1С-Битрикс приложений. Оптимизация php кода

Довелось мне поработать с разными проектами и разными людьми. Бывали проекты нагруженные в плане посещаемости, но бедные по функциональности. Бывали наоборот — 500 хитов в месяц, а сложность функционала — выше крыши. Но вот с чем не повезло — так это с разработкой. Разработать проект с нуля и до запуска мне приходилось буквально пару раз. В остальных случаях приходилось заниматься сопровождением существующего кода (написанного другими разработчиками) и внедрением новых функциональностей в него. Но я считаю это огромным плюсом моей работы. Во-первых — можно встретить множество решений, как интересных, так и откровенно говоря «поганых». Во-вторых, приходится задействовать огромное количество серого вещества с целью собрать воедино всю картину в голове, чтобы при внедрении каких-то доработок не развалились старые.

Часто приходится задумываться и о том, чтобы все «работало быстро», и как привести существующий код, генерирующий 200, 300, 500, 900, а порой и 2-4к запросов к БД на хит, а скорость рендеринга клиентом сгенерированного контента превышает 3-5 секунд.

В этой статье я затрону основные принципы оптимизации битрикс приложения (да, я рассматриваю сайты на битриксе именно как «приложения», а не сайты) со стороны сервера. Итак, начнем

Как анализировать нагрузку на битрикс

Самый первый инструмент, на который вы должны смотреть при оптимизации — это «Отладка» битрикса. Это такая кнопка на административной панели, при клике по которой появляется в нижней части страницы появится информация о скорости генерации страницы и запросах, которые она сгенерировала.

ОтладкаВнизу страницы

При  клике по ссылкам можно увидеть развернутую инфу о том, какие компоненты сколько запросов вызвали и как быстро отработали

Отладка детально

Также в поиске проблем с нагрузкой сильно сможет помочь профилирование приложения. Как правило для этих целей используются такие инструменты как xhprof или xdebug, Их использование я рассмотрю в дальнейших постах, но если вкратце — профилирование, это такой процесс, который записывает вызовы всех инструкций языка и записывает время и порядок их выполнения. В результате профилирования получается файлик, который содержит всю информацию о запуске страницы. Сам по себе такой файлик мало читабелен, но есть множество утилит для визуализации содержимого таких файлов. После профилирования сразу станет понятно, на каких функциях происходят «затыки» по времени выполнения, какие функции вызываются чаще остальных, что позволит сузить область поиска.
На наших серверах обе утилиты установлены и настроены, нужно лишь начать использовать (обычно это делается через get параметр или через куку).
Кстати говоря — профилировочные логи xdebug поддерживается phpStorm из коробки. Достаточно загрузить лог в phpStorm и можно будет проанализировать его в IDE, сразу переходить к функциям из лога. Примерно так выглядит просмотр лога xdebug в phpStorm:

xDebug profiler snapshot

Не стоит забывать и о простых инструментах. Вы же всегда можете замерить скорость работы какого-то участка с помощью простых вызовов ф-ии microtime как-то так:

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

Ниже я не буду писать про экономию «на спичках» вроде подмены foreach на for или array_map, не буду агитировать менять регулярки на строковые функции и т.д. Про это я как-нибудь потом напишу отдельным постом.

Оптимизация по количеству запросов

Первое, что приходит на ум — уменьшить количество возможных запросов до минимума. Представим такую ситуацию — вам нужно отобразить на странице список товаров, а под списком товаров сформировать некую область, которая будет содержать логотипы всех брендов, которым принадлежат товары со страницы. Бренды хранятся в отдельном инфоблоке, у бренда есть название (NAME) и детальное изображение (DETAIL_PICTURE), а у каждого товара есть свойство, в котором указана привязка к элементу инфоблока брендов (PROPERTY_BRAND). Что обычно делает разработчик, который не думает головой:

  • он получает список товаров запросом, где достает название товара и привязку к бренду
  • в цикле по полученному результату получает сами бренды в виде элементов инфоблока
  • А затем занимаются выводом информации

Никогда так не делайте. Это ужасно, мерзко, и неправильно. Был период в жизни, когда реально устал это говорить людям.

Какие я вижу тут проблемы? Ну, во-первых — это собственно запрос на получение брендов в цикле. Если пораскинуть мозгами, то подобные страницы делаются, как правило, для не очень большого количества товаров (200, может быть 300). Я бы в данном случае применил бы другой способ. В цикле по товарам имеет смысл собрать все идентификаторы в массив, и затем использовать этот массив для выполнения одного единственного запроса к БД. Выглядеть это будет примерно так:

Я намеренно здесь использовал для хранения идентификаторов брендов хеш-массив с ключами. Если товаров будет много, то бренды наверняка будут дублироваться в обычном нумерованном массиве, после чего его придется унифицировать (либо на уровне php, либо на уровне БД), а для этого придется обойти весь массив. А при записи в массив с ключами скорость доступа к элементу практически мгновенная (в разумных пределах). Перерасход по памяти явно небольшой (брендов должно быть сильно меньше, чем товаров в нормальных условиях), а вот прирост по скорости работы с массивом будет чувствоваться.

В общем случае лучше сделать один чуть более сложный запрос на выборку всех необходимых данных, чем сотню маленьких запросов, каждый из которых получит только часть таких данных.

Оптимизация по количеству выбираемых данных

В нашем примере выше есть еще один огромный недостаток. По условию задачи нам нужно только изображение бренда. Однако наш запрос получит все возможные поля и свойства из инфоблока с брендами. Не забывайте определять именно нужный набор данных, который вы хотите использовать на странице. Это может сэкономить достаточно много времени при выборках, т.к. битрикс не умеет генерировать оптимальные запросы (в D7 с этим явно получше, но оно пока не доведено до ума и пользуются им единицы пока). Поэтому, чем меньше данных вы выбираете, тем меньше вам битрикс сгенерирует в запросе join’ов, подзапросов, объединений и тем быстрее отработает ваш запрос. На деле это будет выглядеть так:

Хочу отметить еще один момент, которым часто пренебрегают разработчики. У класс CIBlockResult есть метод GetNextElement(). Он возвращает объект _CIBElement, который позволяет получить доступ к полям выбранного элемента и к его свойствам. Не стоит пользоваться методом GetProperties() без полного осознавания того, что он тащит вообще все свойства из БД. Если добавить тот факт, что люди используют его в цикле по всем элементам выборки, то данный участок кода может получиться очень тяжелым. Ограничивайте набор выбираемых свойств и не пользуйтесь GetNextElement() в циклических выборках. Оправданным получение всех свойств является только в исключительных ситуациях (например на детальной странице товара, где частно нужно почти все свойства). Вот пример того, как делать не нужно:

Если уж вы действительно понимаете, что вам необходимо так сделать, то хотя бы ограничивайте перечень свойств, метод GetProperties() содержит 2 параметра — порядок сортировки и фильтр по свойствам, аналогичный CIBlockElement::GetProperty();
И кстати, если даже вы передали в родительском запросе в $arSelect только необходимые вам свойства, то они попадут в GetFields(). А вот GetProperties() будет выбирать только те свойства, которые соответствуют переданному в него фильтру (по умолчанию — все свойства).
Короче — это запрос в цикле, и часто немаленький, поэтому стоит его избегать.

Автокеширование компонентов

Не стоит забывать и о возможностях кеширования в битрикс. Первая и самая простая возможность — автокеширование в компонентах. Базовая ячейка сайта в понятии битрикс — это компонент. Каждая страница состоит из набора компонентов. И каждый компонент, если это не противоречит его логике, должен быть закеширован. Особенно это касается «тяжелых» компонентов, которые занимаются большими выборками данных и их оперированием в долгосрочной перспективе (например, каталог товаров), данные в которых обновляются достаточно редко.
Ну и поскольку данная операция является достаточно часто используемой, то и интерфейс для кеширования в компонентах достаточно прост. Достаточно код, который должен быть закеширован, обернуть в блок кода:

По умолчанию кеш зависит от набора входящих параметров. Т.е. если у компонента будет один параметр с каким-то значением, то именно для этого набора параметров и будет сгенерирован кеш. При изменении этого параметра будет сформирован другой кеш для компонента. В качестве времени для кеша будет браться по умолчанию значение параметра CACHE_TIME компонента. При желании его можно изменить, передав первый параметр методу StartResultCache( $cacheTime ).

Также, помимо параметров, можно еще и добавить зависимостей к идентификатору кеша с помощью второго параметра функции. Например, чтобы сделать кеш зависимым от группы пользователя, можно использовать следующую конструкцию:

Важно понимать, что автоматическое кеширование по умолчанию сохраняет только html вывод, который обрамлен блоком с вызовом метода StartResultCache(). Соответственно, подключение шаблона также должно находиться внутри этого блока. Для того, чтобы включить шаблон в кеш, нужно подключение шаблона загнать  внутрь нашего блока:

Но можно в автоматический кеш компонента битрикс включить также и данные. Для этого нужно использовать метод SetResultCacheKeys(). Данный метод принимает на вход массив ключей, которые содержатся в $arResult. Если вы хотите сохранить в кеш компонента какую-то строку, например название товара (чтобы использовать его после выполнения компонента, например в component_epilog.php), который выводит компонент, то вам нужно использовать следующую конструкцию:

Есть еще один тонкий момент. Важно избегать кеширования ошибочных данных. Если в процессе работы есть шанс получить некорректные данные, то вы должны обрабатывать данную ситуацию и сбрасывать автокеширование. Иначе в кеш запишется некорректный набор данных + шаблон и в течение всего времени жизни кеша пользователь будет видеть неверные данные. Для того чтобы сбросить кеш, используем следующую конструкцию:

Важно понимать, что при закешированном компоненте его результат отдается из кеша, и весь код, который написан в коде компонента внутри блока кеширования, а также весь код шаблона исполняться повторно не будет, поэтому если вы вдруг используете отложенные функции внутри шаблонов компонентов (или в result_modifier.php), то при кешировании они работать не будут. Поэтому никогда не используйте отложенные функции в шаблоне компонента (а точнее в файлах result_modifier.php и template.php). Если все же нужно использовать отложенные функции, вы можете произвести их вызов в файле component_epilog.php шаблона, т.к. его вызов не кешируется.

Вот в принципе и все, что хотелось сказать про автокеширование компонентов битрикс.
В будущих статьях я постараюсь шире раскрыть эту тему, т.к. в битриксе есть еще крутая штука — тегированный кеш. Напрямую она к оптимизации не относится, но к кешированию — точно относится.

Движок кеширования в битрикс

Компоненты — не единственное что есть в битриксе. Соответственно это все тоже нуждается в кешировании. Для этого в битриксе есть специальный класс — CPHPCache. Он является оберткой над другими типами кеширования, которые поддерживает битрикс (APC, Memcached, файловый кеш). Хранилище для кеша задается в настройках битрикса.

Использовать его крайне просто:

Данный класс может также кешировать и html вывод. Для начала кеширования html нужно вызвать метод StartDataCache(). Для вывода сохраненного html можно использовать метод Output(). Доработаем наш пример:

Более подробно с классом кеширования можно познакомиться на страницах документации.

HTML кеширование (или композит)

В битрикс уже давно существует возможность кеширования страниц целиком. Суть механизма в том, чтобы тупо сохранить сгенерированную страницу в html формате и при последующих хитах отдавать именно сохраненный html, не нагружая при этом БД и не исполняя код. Однако до недавнего времени у этой технологии были небольшие проблемы, в т.ч. и с обновлением данных. Данные в кеше либо слишком часто обновлялись, либо не обновлялись вообще, в большей степени по причине непонимания работы технологии разработчиками. Поэтому часто можно видеть данный механизм отключенным на сайте.

На смену html кешированию пришла технология «Композитный сайт». По сути это то же самое, только в новой обертке и с несколько улучшенным и усложненным функционалом.
Идея его в том, чтобы сохранить страницу в html и отдавать при следующем обращении эту html, а для поддержания актуальности данных на странице для пользователя после загрузки в асинхронном режиме выполняются ajax запросы, которые подтягивают данные в динамических блоках (вроде корзины пользователя или рандомного блока со списком новинок).

Благодаря этой технологии скорость отдачи сервером html кода может достигать 10 мс, что в принципе очень быстро (гугл рекомендует отдавать весь контент до 200 мс, т.е. это быстрее рекомендаций гугла в 20 раз).

При настройке композитного режима нужно провести большую, но рутинную работу. В большинстве случаев она простая, но иногда придется поломать голову. Сводится эта работа к доработке кода шаблона компонентов. Для того чтобы композит заработал на сайте, нужно чтобы все компоненты «поддерживали» композитный режим работы (или как говорят в битрикс — компоненты и шаблоны должны «голосовать за» композитный режим). Сам компонент по умолчанию голосует «за» (за исключением пары стандартных компонентов). Однако шаблон — по умолчанию — против. Это сделано для того, чтобы разработчик сам решал, возможно ли для конкретного шаблона настроить композит, ведь по большей части при работе с битриксом приходится работать именно с шаблонами. Для того, чтобы шаблон компонента проголосовал «за», нужно вызвать метод setFrameMode(), как-то так:

Этим вызовом вы сообщаете, что ваш шаблон является статическим, т.е. не содержит информации, которая изменяется от каких-либо специфичных условий, персональных для пользователя или каких-то случайных чисел. Html код данного шаблона будет записан в кеш.

Если шаблон вашего компонента является полностью динамическим, и его нужно всегда поддерживать в актуальном виде, то вместо вышеуказанной конструкции пишете вот эту в самом начале шаблона:

Код, который содержится под этим вызовом и до конца шаблона будет исполняться всегда, но уже на повторном хите, который произойдет через ajax. На тот период, пока страница отображается и делается ajax запрос (а это доли секунды, часто незаметные для глаза), будет показан контент, который был сгенерирован на предыдущем хите для этого компонента. Можно задать и свою «заглушку», для этого достаточно задать строковый параметр методу begin, например так:

Если ваш компонент содержит несколько таких динамических зон, то можно написать так, главное чтобы не было вложенных друг в друга зон:

И таких блоков может быть несколько в компоненте.

Есть еще определенные тонкости при работе с композитом, но я опять же не хотел бы затрагивать их в этой статье, т.к. эта тема достаточно обширна и для нее у меня тоже уже есть заготовка.

Вот пожалуй и все на этот раз, спасибо за внимание

  • Ilya Nekrasov

    На днях придумал как по-красивее решить проблему с запросами в цикле описанную в этом посте.
    Накидал пакет для композера https://github.com/arrilot/bitrix-collectors

  • Александр

    Доброго времени суток, подскажите, пожалуйста, на какой бюджет нужно ориентироваться по оптимизации скорости загрузки среднего интернет магазина? Хотя бы приблизительную планку можете обозначить?

    • У всех людей разное понятие «среднего» интернет-магазина. Более важное значение имеет технологическая платформа, на которой работает проект, а также тот факт, насколько хорошо разработчики следовали принципам разработки на этой технологической платформе, да и вообще уровень их компетенции.
      Также очень многое зависит от вида тормозов, которые испытывает проект.
      Поэтому я не смогу дать ответ на ваш вопрос.

      По опыту скажу, что бывают «тяжелые» случаи, когда переработка всего проекта может оказаться дешевле его оптимизации, и наоборот, казалось бы, тяжелейший случай можно решить всего лишь в течение дня.

      Если хотите просчитать стоимость оптимизации скорости загрузки какого-то конкретного проекта — обратитесь в веб-студию, они вам посчитают и с удовольствием сделают.

  • Николай Волков

    А если выбирать всего 1 элемент (‘nTopCount = 1’) из «толстого» инфоблока посредством getList с фильтром по 2-м свойствам (IBLOCK_ID, PROPERTY_USER_ID)? У меня такая выборка ложит сайт. Есть решение, или выкидываем этот битрикс туда где бомжи роются?

    • Вы бы привели для наглядности весь код вызова CIBlockElement::GetList(). Теоретически добавление в 4м параметре этого метода массива, содержащего элемент nTopCount = 1 приведет к добавлению LIMIT=1 в итоговом запросе.
      Как альтернативу можно использовать одну из существующих ныне оберток для ORM, которая позволяет работать со свойствами. Моя реализация лежит на гитхабе https://github.com/maximaster/tools.orm

      Выкинуть битрикс на помойку — довольно заманчивая перспектива 🙂 Если у вас есть такая возможность, то лучше именно так и поступить. Не из-за той проблемы, с которой вы столкнулись, а в принципе

      • Николай Волков

        https://uploads.disquscdn.com/images/75d689439f29d5e2f931f80cec3247aac9b12c5a8c1c8a592a55e9104b28e24a.jpg
        Как то так)))
        Ну вообще я давно уже рвусь переписать все на Laravel или Symfony 2, останавливает сейчас только битриксовый магазин (его надо будет написать самому, но не люблю я e-commerce)

        • Я надеюсь, $arCompany[‘ID’], $arLayer[‘ID’] и $arUser[‘ID’] не являются пустыми? 🙂 Константа IBLOCK_COMPANY существует?
          Еще бы я все-таки заменил !%ID на !ID без модификаторов фильтра. По доке должно приводить к одному результату, но на деле внутрянка может работать по другому. Проставление символа % в поле фильтра обычно приводит к регистронезависимому поиску, тогда как для ID это эквивалентно точному совпадению с искомым значением.
          Ну и смотреть логи, что валится при этом запросе — веб-сервер или БД, смотреть запрос, сформированный битриксом при данном вызове и анализировать его с explain, если проблема в долгом времени выполнения запроса.

          • Николай Волков

            все вводные для фильтра конечно заданы))
            насчет !%ID и !ID, хороший совет, надо попробовать.
            по поводу анализа, тут каюсь, не проводил, все времени не было, займусь. Спасибо!
            PS: maximaster/tools.orm классная штука, взял на заметку))

  • Сергей

    Дополнение, касательно пункта по оптимизации по количеству запросов. В вашем примере не обязательно сначала выбирать идентификаторы брендов, а потом их свойства. Можно сделать проще:

    • Количество запросов, вроде бы, не изменится, но в вашем случае это запросы с join. Плюс ко всему — кода чуть больше, т.к. внутри цикла по товарам нужно будет отдельно проверять, был ли выбран уже данный бренд ранее. И еще один минус — формирование информации о брендах производится внутри цикла по товарам, что может уменьшить читабельность, но это конечно уже мелочи.
      Надо будет сравнить по времени, кстати, эти 2 варианта 🙂
      Есть у меня в голове еще один вариант, позже добавлю его в пост.

  • sergu73

    «По-человечески» понятно написано, спасибо )

  • Алексей

    Спасибо. Гораздо лучше написано, чем на сайте битрикса.