Если вы уже разобрались, что такое LCP, CLS и INP, прогнали сайт через PageSpeed Insights и увидели красные цифры — дальше начинается самое сложное: понять, что именно в коде и конфигурации Битрикса даёт эту просадку. Общие советы вида «оптимизируйте картинки» и «включите кэш» здесь не работают, потому что на реальном проекте узкое место почти всегда прячется в чём-то одном конкретном — обработчике события, чужом виджете, незакэшированном запросе к каталогу.
Эта статья — не про основы метрик, а про разбор пяти-шести конкретных технических причин, из-за которых сайт на 1С-Битрикс реально тормозит, с указанием, как их найти профилировщиком и как исправить. Если основы ещё не освоены — сначала прочитайте статью «Core Web Vitals для интернет-магазина», там разобраны сами метрики и общие рычаги, а здесь мы идём сразу в диагностику. Материал собран на опыте наших проектов по оптимизации Core Web Vitals на Битрикс.
Коротко
- Обработчики событий ядра (
OnAfterEpilogи другие), которые выполняют тяжёлый код на каждом хите — частая и незаметная причина медленного TTFB. - Синхронные сторонние скрипты и виджеты без
async/deferблокируют рендер и напрямую бьют по LCP и INP. - Неограниченные
CIBlockElement::GetListв шаблонах компонентов — классическая причина тяжёлых запросов на страницах каталога. - Стратегия загрузки веб-шрифтов (FOIT/FOUT) определяет, будет ли «прыгать» текст и терять CLS.
- Композитный кэш — не опция «для галочки»: без него каждая страница без кэша компонентов собирается заново на PHP.
С какой точки вести диагностику
Прежде чем чинить что-то конкретное, разделите два разных вопроса: «медленно грузится HTML» (это про TTFB и бэкенд) и «страница долго становится интерактивной или дёргается при загрузке» (это про фронтенд и рендер). Смешивание этих двух категорий — главная причина, почему разработчики тратят время не на то. Оптимизация JPEG-картинок не спасёт, если узкое место — необработанный обработчик события, съедающий 800 мс на каждом хите ещё до того, как браузер получил первый байт HTML.
Практический порядок:
- Снимите полевые данные. Посмотрите реальные метрики пользователей — через Яндекс.Метрику, Google Search Console (отчёт Core Web Vitals) или собственный сбор web-vitals.js. Это покажет, какая метрика реально проблемная — LCP, CLS или INP — и на каких шаблонах (главная, каталог, карточка).
- Включите отладочную панель Битрикса. В админке — «Настройки → Инструменты разработчика → Панель веб-мастера», либо константа
BX_XML_DEBUG/показ отладочной информации для конкретного пользователя. Панель даёт время выполнения по компонентам, число SQL-запросов и хиты кэша. - Прогоните целевую страницу через профилировщик. Встроенный модуль «Монитор производительности» (в редакциях старше «Старта») или сторонний Xdebug/Blackfire на тестовом окружении — чтобы увидеть, где именно тратится время выполнения PHP.
- Проверьте Network-вкладку браузера. Отдельно от бэкенда: какие ресурсы блокируют рендер, когда приходит первый байт (TTFB), когда рендерится LCP-элемент, и что происходит между этими моментами.
- Сузьте до конкретной причины. Дальше идите по разделам ниже — они покрывают самые частые конкретные виновники на проектах на Битрикс.
Обработчики событий на каждом хите
1С-Битрикс построен на событийной модели: любой модуль может подписаться на системное событие через AddEventHandler и выполнить свой код в определённый момент жизненного цикла хита. Это удобно для разработки, но именно здесь чаще всего прячется просадка TTFB на, казалось бы, «оптимизированном» сайте.
Особенно опасны события, которые срабатывают на каждой странице сайта, независимо от того, какой компонент выводится:
OnAfterEpilog. Срабатывает в самом конце формирования страницы, уже после того, как контент собран. Модули аналитики, счётчики, трекеры часто вешают сюда код — и если внутри есть запрос к внешнему API без таймаута или тяжёлый SQL, это добавляется к времени ответа каждого хита.OnBeforeProlog/OnProlog. Выполняется до вывода страницы — здесь часто живут проверки региона, персонализации, редиректы. Тяжёлая логика тут особенно чувствительна, потому что блокирует всё, что идёт дальше.- События модуля продаж (
OnSaleOrderSavedи аналоги). На первый взгляд не должны влиять на скорость каталога — но если разработчик повесил на них синхронную отправку данных во внешнюю CRM или 1С без очереди, при пиковой нагрузке это замедляет сохранение заказа и создаёт ощущение зависшего сайта именно в момент оформления, что напрямую бьёт по INP на самой конверсионной странице.
Диагностика простая: в профилировщике или через ручное логирование времени внутри init.php засеките суммарное время выполнения всех обработчиков конкретного события. Если на нейтральной статической странице (например, «О компании») время выполнения PHP заметно больше нуля и не объясняется явным выводом компонентов — почти наверняка виноват один из обработчиков, повешенный на общее событие.
Как чинить. Аудит всех подключённых обработчиков (файл .settings.php модулей и init.php), вынос тяжёлых операций из синхронного пути в очередь (агенты, отложенные задания) и обязательные таймауты на любые внешние HTTP-вызовы внутри обработчиков. Если обработчик нужен только для конкретных страниц — проверяйте контекст ($APPLICATION->GetCurPage() или аналог) и выходите раньше, а не выполняйте логику вслепую на каждом хите.
Чужие include и CModule::IncludeModule в рендере
Второй частый источник просадки — модули и виджеты, которые подключаются прямо в шаблоне сайта или в компоненте через CModule::IncludeModule('modulename') и затем делают собственные запросы к базе или к внешним сервисам синхронно, в основном потоке рендера страницы. Каждый такой вызов — это не просто загрузка файла модуля, а потенциально целая цепочка инициализации: собственные обработчики событий, собственные запросы конфигурации, иногда собственные HTTP-запросы наружу (проверка лицензии, получение баннера, синхронизация настроек).
Особенно заметно это на страницах, где такой инклюд стоит в общем шаблоне (header.php, footer.php) и выполняется буквально на каждой странице сайта — включая те, где виджет никогда не используется. Классический пример — модуль онлайн-консультанта или виджет отзывов, интегрированный не через асинхронный JS-сниппет, а через PHP-компонент с собственной логикой на сервере.
CModule::IncludeModule с заметным временем выполнения на страницах, где логика этого модуля визуально не нужна. Второй признак — модуль, установленный «для теста» полгода назад и забытый, но чьи файлы всё ещё подключаются в общем шаблоне.
Фикс здесь почти всегда один: перенести интеграцию с уровня PHP-инклюда на уровень клиентского JavaScript-сниппета, загружаемого асинхронно и по возможности отложенно (после первого взаимодействия пользователя или через небольшую задержку после DOMContentLoaded). Серверная часть должна остаться только там, где без неё правда не обойтись — например, для персонализации, зависящей от данных сессии.
Некэшируемые сторонние виджеты и скрипты
Чаты, виджеты отзывов, всплывающие формы захвата, сервисы веб-аналитики сверх стандартных счётчиков — на среднем интернет-магазине таких интеграций набирается пять-семь. Каждая тянет свой JS, часто с CSS и шрифтами, и если хотя бы один из них подключён синхронным тегом <script src="..."> без async или defer в разделе <head>, браузер останавливает парсинг страницы до полной загрузки и выполнения этого файла.
Эффект на метрики конкретный: блокирующий скрипт в <head> откладывает момент, когда браузер вообще начинает рендерить видимый контент — это прямой удар по LCP. Если скрипт при этом внедряет свой блок в DOM после загрузки (баннер, плашка согласия), не зарезервировав для него место заранее через CSS — это удар по CLS: layout «прыгает», когда блок внезапно появляется.
Отдельная категория проблем — виджеты, которые сами по себе выполняют тяжёлую работу в основном потоке JS уже после загрузки: собирают детальную аналитику поведения, перехватывают события скролла и клика без дебаунса. Это не влияет на LCP, зато напрямую бьёт по INP — браузер занят выполнением чужого кода в момент, когда пользователь пытается нажать кнопку «В корзину».
| Симптом | Вероятная причина | Что проверить |
|---|---|---|
| LCP растёт, TTFB в норме | Блокирующий скрипт в <head> | Порядок и атрибуты <script> в head.php |
| CLS скачет на загрузке | Виджет вставляет блок без резерва места | min-height/aspect-ratio под будущий блок |
| INP плохой при кликах после загрузки | Тяжёлые обработчики в основном потоке JS | Long tasks в DevTools Performance |
| Просадка только на мобильных | Виджет не адаптирован, тяжёлый рендер на слабом CPU | Профиль CPU throttling в Lighthouse |
Практическое правило: любой сторонний скрипт, без которого страница функционально работает (чат, аналитика, баннеры), должен загружаться async или defer, а в идеале — с отложенным стартом (через requestIdleCallback, таймер в несколько секунд или загрузку по первому скроллу/клику). Это не требует переписывать сам виджет — только способ его подключения в шаблоне.
Веб-шрифты: FOIT, FOUT и просадка LCP/CLS
Если основной шрифт сайта — не системный, а подключённый веб-шрифт (Google Fonts или собственные .woff2), то стратегия его загрузки напрямую участвует в двух метриках сразу. Браузер по умолчанию (в зависимости от настройки font-display) выбирает одно из двух поведений:
- FOIT (Flash of Invisible Text). Текст невидим, пока шрифт не загрузится. Если LCP-элемент — это текстовый блок (а на большинстве магазинов это заголовок H1 или название товара), таймер LCP не «стартует», пока шрифт не готов — прямая потеря времени.
- FOUT (Flash of Unstyled Text). Текст сразу показывается запасным шрифтом, а затем заменяется на целевой. Быстрее для LCP, но если метрики шрифтов (ширина символов, межстрочный интервал) заметно отличаются от запасного — при подмене «прыгает» layout, и это бьёт по CLS.
На практике правильная стратегия — компромисс: font-display: swap или optional вместе с тщательно подобранным fallback-стеком (системный шрифт с похожими метриками ширины символов) и, если возможно, предзагрузкой критичного файла шрифта через <link rel="preload" as="font"> с корректным crossorigin.
Неограниченные CIBlockElement::GetList в шаблонах
Это, пожалуй, самая известная разработчикам Битрикс причина медленного бэкенда — и всё равно одна из самых частых на реальных проектах, особенно после нескольких лет доработок разными подрядчиками. Проблема в том, что вызов CIBlockElement::GetList (или его D7-аналог через \Bitrix\Iblock\ElementTable::getList) вставляется прямо в файл шаблона компонента или в кастомный component_template.php — то есть выполняется на каждый показ страницы, часто без ограничения количества и без кэширования.
Типичные варианты, которые встречаются на аудитах:
- Блок «похожие товары» или «с этим товаром покупают» в шаблоне карточки товара, который каждый раз запрашивает весь раздел каталога и фильтрует результат в PHP, вместо того чтобы ограничить выборку условием в самом запросе.
- Вывод «последних новостей» или «популярных категорий» в подвале сайта — общем для всех страниц, — реализованный прямым запросом без
CACHED_QUERYи без ограниченияnPageSize. - Вложенные запросы: для каждого элемента списка товаров отдельно запрашиваются его свойства или связанные элементы отдельным
GetListвнутри цикла — классическая проблема N+1, знакомая по любой ORM, только здесь она особенно незаметна, потому что каждый отдельный запрос быстрый, а их суммарное число — нет.
Как чинить. Всегда указывайте явные лимиты (nPageSize в параметрах или limit в D7), используйте фильтр ACTIVE_DATE/ACTIVE для отсечения неактивных элементов на уровне SQL, а не PHP, и оборачивайте такие блоки в кэш компонента или ручной CPHPCache/Bitrix\Main\Data\Cache с тегированной инвалидацией. Для вложенных запросов на свойства — используйте один запрос с GetPropertyValuesArray или JOIN через D7 вместо цикла.
Отсутствующий или сломанный композитный кэш
Композит — штатный механизм Битрикса, который отдаёт статически сохранённую HTML-версию страницы почти мгновенно, а динамические зоны (авторизация, корзина, персональные блоки) подгружает отдельными AJAX-запросами поверх статики. Если основы этого механизма и его связь с кэшированием уже знакомы — подробный разбор уровней кэша есть в статье «Кэширование в 1С-Битрикс»; здесь — только то, что касается конкретно Core Web Vitals.
На реальных аудитах композит часто либо не включён вообще, либо включён, но фактически не работает — и оба случая выглядят одинаково снаружи: страница грузится заметно дольше, чем «мгновенно». Частые причины поломки композита именно в контексте скорости:
- Слишком короткий или неверно настроенный TTL. Композитная копия устаревает быстрее, чем истекает реальное время между визитами, и почти каждый хит собирает страницу заново.
- Динамические зон слишком много или они слишком «тяжёлые». Если в динамику вынесено больше, чем нужно (например, весь блок с товарами вместо только счётчика корзины), выигрыш от композита частично теряется — браузеру всё равно приходится ждать AJAX-догрузку существенной части контента.
- Персонализация ломает кэшируемость. Если в статическую часть страницы попадают персональные данные (имя пользователя, регион, персональная цена) без вынесения в динамическую зону, композит либо не может закэшировать страницу вовсе, либо — хуже — кэширует персональные данные одного пользователя и отдаёт их другому.
Эффект на метрики прямой: без рабочего композита каждый запрос без кэша компонентов — это полная сборка страницы на PHP, что напрямую отражается на TTFB и, соответственно, на LCP (браузер физически не может начать рендер, пока не получит HTML).
Как выглядит диагностика на схеме
Собрав всё описанное выше в единый порядок действий, диагностика узкого места выглядит как последовательность проверок, где каждый шаг отсекает одну категорию причин:
Реализация в 1С-Битрикс: где смотреть в админке
Ключевые точки диагностики и настройки живут в нескольких разделах админки, и полезно держать их адреса под рукой при аудите:
- Панель веб-мастера / отладка. «Настройки → Инструменты разработчика → Панель веб-мастера» — включает нижнюю отладочную панель с временем выполнения, числом запросов и хитами кэша по каждому компоненту на странице.
- Настройки кэширования. «Настройки → Настройки продукта → Автокэширование» — здесь включается/выключается композит, задаются TTL и управляемый кэш. Именно тут чаще всего находится «композит выключен после последнего обновления».
- Список обработчиков событий. Штатного единого списка в интерфейсе нет — обработчики регистрируются в коде (
init.php, файлы.settings.phpмодулей), поэтому аудит здесь — это просмотр кода, а не клик по админке. Это стоит явно проговаривать с разработчиком при передаче проекта на поддержку. - Монитор производительности. В редакциях с этим модулем — отдельный раздел, показывающий динамику времени ответа и число запросов на популярных URL за период, полезен для отслеживания регрессий после релизов.
Таблица приоритетов: что чинить в первую очередь
Не все причины одинаково окупаются по времени на фикс. Вот ориентир, с чего начинать при ограниченном бюджете на оптимизацию:
| Причина | Эффект на метрики | Сложность фикса | Приоритет |
|---|---|---|---|
| Композит выключен/сломан | TTFB, LCP — сильный на всех страницах | Низкая-средняя | Высокий |
| Тяжёлый обработчик на общем событии | TTFB — сильный на всех страницах | Средняя | Высокий |
| Неограниченный GetList в шаблоне | TTFB на конкретных страницах | Низкая | Высокий |
| Блокирующий сторонний скрипт | LCP, INP | Низкая | Высокий |
| Стратегия загрузки шрифтов | LCP, CLS | Низкая | Средний |
| Чужой PHP-инклюд в общем шаблоне | TTFB на всех страницах | Средняя-высокая | Средний |
Пошаговый аудит перед фиксом
Чтобы не чинить наугад, полезно провести структурированный аудит перед тем, как трогать код. Вот порядок, который мы используем на проектах:
- Соберите список ключевых шаблонов страниц. Главная, категория каталога, карточка товара, корзина, оформление заказа — на каждом типе страницы узкие места могут быть разные.
- Прогоните каждый шаблон через отладочную панель. Зафиксируйте суммарное время, число SQL-запросов и список компонентов без кэша (MISS).
- Отсортируйте компоненты по вкладу во время ответа. Обычно один-два компонента дают 70–90% суммарного времени — начинайте с них.
- Проверьте фронтенд отдельно. Lighthouse в режиме «Mobile» с троттлингом CPU и сети — это ближе к реальному опыту пользователя, чем «Desktop» без ограничений.
- Зафиксируйте базовые цифры до фикса. Без цифр «до» невозможно доказать, что конкретное изменение реально помогло, а не просто «показалось быстрее».
- Правьте по одному фактору и перемеряйте. Одновременное изменение пяти вещей не даёт понять, какая из них дала эффект, а какая — нет.
Частые ошибки диагностики и фикса
- Оптимизируют картинки и подключают lazy-loading, не проверив TTFB — если бэкенд отдаёт HTML за полторы секунды, экономия на картинках погоды не делает.
- Меряют скорость только в «Desktop»-режиме Lighthouse, хотя основной трафик магазина — мобильный с более слабым CPU и медленной сетью.
- Включают композит «для галочки», не проверив, что персональные данные действительно вынесены в динамические зоны — рискуют показать чужие данные другому пользователю.
- Правят сразу несколько причин одновременно и не могут потом объяснить заказчику, какое именно изменение дало эффект.
- Игнорируют полевые данные (реальных пользователей) и опираются только на разовый прогон PageSpeed с одного сервера — лабораторные данные не всегда совпадают с реальным опытом на разных устройствах и сетях.
- Переписывают GetList на D7 «для красоты кода», не добавив ограничение выборки и кэш — структура запроса меняется, а проблема (нет лимита, нет кэша) остаётся.
- Оставляют отладочную панель включённой для всех посетителей на боевом сайте — она сама по себе добавляет накладные расходы и раскрывает техническую информацию.
Чек-лист перед повторным замером
Перед тем как заявлять об улучшении метрик заказчику или команде, убедитесь, что все пункты закрыты:
- Зафиксированы базовые цифры. TTFB, LCP, CLS, INP и число SQL-запросов сняты до начала работ на тех же тестовых условиях.
- Композит проверен и работает корректно. Персональные данные не попадают в статическую часть, TTL адекватен трафику.
- Обработчики событий на общих событиях аудированы. Тяжёлая логика вынесена из синхронного пути или получила проверку контекста.
- Все GetList в кастомных шаблонах ограничены и закэшированы. Нет вложенных запросов в цикле (N+1).
- Сторонние скрипты подключены асинхронно. Ничего блокирующего в <head> без явной необходимости.
- Стратегия загрузки шрифтов задана осознанно. font-display и подобранный fallback-стек прописаны в CSS.
- Замер повторён на тех же условиях. Тот же тип устройства, та же сеть, тёплый и холодный кэш отдельно.
Вывод
Плохие Core Web Vitals на 1С-Битрикс почти никогда не бывают «просто сайт тяжёлый» — за просадкой обычно стоит одна-две конкретные технические причины: обработчик события на общем хуке, незакэшированный GetList, блокирующий сторонний скрипт или сломанный композит. Диагностика через профилировщик и отладочную панель занимает часы, а не недели, и почти всегда даёт понятный список конкретных фиксов вместо расплывчатого «надо оптимизировать». Именно такой точечный подход даёт устойчивый прирост метрик, который не откатывается после следующего релиза.