Перейти к содержимому

Композит изнутри - статическая копия, зоны, пересборка

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

Механика

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

Статическая копия - это готовый HTML-файл страницы в каталоге /bitrix/html_pages/<домен>/. Кэш компонента хранит результат одного вызова, а страницу вокруг него всё равно собирает платформа. Копия же уходит посетителю целиком и раньше, чем ядро успевает стартовать.

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

Второе обращение уже получает её целиком, и порядок величин тут примерно пятнадцать миллисекунд против семисот пятидесяти на первом. Ответ «не изменилось» появляется лишь с третьего обращения: второе отдаёт дату изменения, третье присылает её обратно.

Страница делится надвое границами динамических зон. Всё между $this->createFrame()->begin() и $frame->end() выполняется на каждом обращении, а разметка вокруг попадает в общую копию. Вкладывать зоны друг в друга платформа не разрешает.

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

«Моргание» зоны - прямое следствие такого порядка. Сначала посетитель видит заглушку из копии, например счётчик корзины с нулём, и лишь потом приходят настоящие данные. Сглаживают это заглушкой той же структуры и плавным появлением, а мгновенный показ личного делают через BX.localStorage.

Каждый компонент страницы голосует за композит или против него. Компонент, который нельзя кэшировать, ставит $this->setFrameMode(false), и одного такого голоса хватает на отключение всей страницы. По умолчанию против голосуют bitrix:lists, bitrix:sale.personal.subscribe и bitrix:search.page.

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

Внутри кэшируемой области часть вызовов ведёт себя иначе. FrameBuffered буферизирует содержимое, и отложенные функции вроде ShowTitle там не работают. А bitrix_sessid_post() в кэшируемом шаблоне отдаёт пустое значение, которое подставит уже JavaScript.

Шаги

  1. Замерить сборку страницы без композита, добавив к её адресу отладочный параметр ncc=1.
  2. Сравнить этот замер со вторым обращением к той же странице, у которой копия уже готова.
  3. Пройти по разметке глазами гостя и вошедшего покупателя, выписывая все различающиеся места.
  4. Проверить голосование компонентов страницы и найти шаблоны, которые голосуют против кэширования.
  5. Посмотреть частоту перезаписи копии по числу соседних файлов с меткой времени в имени.

Код

Смотрим статическую копию на диске:

Окно терминала
ls -la /home/site/public_html/bitrix/html_pages/example.org/catalog/
# page.html - сама копия, которую получают посетители
# page.html.delete.1509433132.4885 - прежняя версия, сохранённая перед перезаписью
ls /home/site/public_html/bitrix/html_pages/example.org/catalog/ | grep -c delete
# столько раз копия этой страницы переписывалась с момента создания каталога
curl -sI 'https://example.org/catalog/?ncc=1' | grep -i x-bitrix-composite
# параметр ncc=1 отменяет композит: служебного заголовка в ответе не будет

Число файлов с меткой времени в каталоге страницы прямо показывает, сколько раз копия переписывалась. Растущая пачка означает, что в разметку попало что-то нестабильное.

Размечаем границу личной зоны в шаблоне:

$frame = $this->createFrame()->begin('0'); // заглушка попадёт в статическую копию
?><span class="cart-count"><?= $cartCount ?></span><?php
$frame->end();
// код между begin() и end() выполняется на каждом хите, а не при сборке копии
// begin() без аргумента подставит заглушкой данные предыдущего хита

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

Обходим ограничение буферизованной зоны:

$area = new \Bitrix\Main\Page\FrameStatic('workarea');
$area->setAssetMode(\Bitrix\Main\Page\AssetMode::STANDARD); // стили и скрипты минуют копию
$area->setAnimation(true); // плавное появление вместо рывка
$area->setStub('загрузка');
$area->startDynamicArea();
$APPLICATION->IncludeComponent('mycompany:mycomponent', 'template', []);
$area->finishDynamicArea();

Зона без буферизации расставляет метки и забирает содержимое после выполнения страницы. Отложенные функции внутри неё работают, а условное подключение стилей перестаёт переписывать копию.

Разделяем содержимое зоны и заглушку:

$frame = new \Bitrix\Main\Page\FrameBuffered('cart-line');
$frame->begin();
// содержимое зоны: приходит фоновым запросом на каждом обращении
$frame->beginStub();
// заглушка: именно она попадает в статическую копию страницы
$frame->end();
// отложенные функции внутри буферизованной зоны не работают

Разделение выручает там, где заглушка сложнее одной строки текста. Всё до переключения на заглушку в копию не попадает, а всё после отдаётся посетителям одинаковым.

Убираем чужой голос против:

$cache = \Bitrix\Main\Data\StaticHtmlCache::getInstance();
$cache->disableVoting();
$APPLICATION->IncludeComponent('bitrix:search.page', '', []); // голосует «против»
$cache->enableVoting();
// свой компонент честнее пометить в его коде вызовом setFrameMode(false)

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

Объявляем зону прямо в шаблоне сайта:

$frame = \Bitrix\Main\Page\Frame::getInstance();
$frame->startDynamicWithID('header-auth');
// личный блок шапки: имя посетителя, вход, ссылка на кабинет
$frame->finishDynamicWithID('header-auth', '');
// setAutoUpdate(false) вместе с setAutoUpdateTTL(60) отключает фоновый запрос

В шапке и подвале объекта компонента нет, поэтому зону создают через одиночный экземпляр. Режим без фонового запроса подходит лендингам, на которых личного содержимого нет вовсе.

Убираем из разметки то, что переписывает копию:

// каждое из этих значений своё у посетителя и заставляет пересобрать копию
echo $USER->GetID(); echo bitrix_sessid_post(); echo rand(0, 9999);
echo $_SERVER['HTTP_USER_AGENT'];
echo '<input type="hidden" name="back_url" value="' . $_SERVER['REQUEST_URI'] . '">';
// стабильные замены: BX.message('USER_ID'), BX.message('bitrix_sessid'),
// метод $this->randString(), а разбор браузера - в коде компонента

Контрольная сумма считается по всей странице целиком. Одно меняющееся значение в разметке возвращает копию к состоянию «пересобирается на каждом обращении».

Возвращаем обработчики после догрузки зоны:

if (window.frameCacheVars !== undefined) { // страница пришла из копии
BX.addCustomEvent('onFrameDataReceived', function (json) {
initSliders(); // события на элементах зоны потеряны
});
} else {
BX.ready(initSliders); // обычная загрузка без композита
}
BX.addCustomEvent('onBeforeDynamicBlockUpdate', function () { /* зона вот-вот сменится */ });
BX.addCustomEvent('onFrameDataRequestFail', function () { /* фоновый запрос не дошёл */ });

Замена содержимого зоны выбрасывает старые узлы вместе с их обработчиками. Скрипт, привязанный к разметке зоны при загрузке страницы, после догрузки просто перестаёт работать.

Запрещаем копию для отдельной страницы:

\Bitrix\Main\Data\StaticHtmlCache::getInstance()->markNonCacheable();
// вызов из любой части страницы: в статику она уже не попадёт

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

Чистим старые файлы по расписанию:

Окно терминала
php -f /home/site/public_html/bitrix/modules/main/tools/cron_html_pages.php 10

Задание удаляет файлы кеша старше десяти часов и держит каталог в разумном объёме. Каталог html_pages руками не чистят: после ручного удаления приходится отдельно приводить в порядок служебные файлы.

Ограничения

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

Копия заводится на каждый адрес отдельно. Каталог с умным фильтром порождает копию на каждую комбинацию параметров, и объём каталога html_pages растёт быстрее ожиданий.

Права доступа копия не проверяет. Раздел, закрытый от неавторизованных посетителей, композитом не защищается: готовый HTML уходит раньше проверки доступа. Такие разделы выводят из композита масками исключений.

Мимо композита идут запросы не методом GET, адреса внутри /bitrix, запросы с cookie _NCC или параметром ncc, а также вызовы через BX.ajax. Страницы с админ-панелью не кэшируются, и это норма, а не поломка.

Хранение зоны в браузере через setBrowserStorage(true) рабочим считать больше нельзя. Механизм опирался на Web SQL, а его убрали из современных браузеров, и в Firefox его не было никогда.

Типичные проблемы

Счётчик корзины сначала показывает ноль, потом правильное число.

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

Слайдер и меню периодически перестают работать.

Догрузка зоны заменила её разметку целиком, а обработчики висели на старых узлах страницы. Их вешают заново по событию получения данных, а не при обычной готовности документа.

Композит выключился сразу на всей странице.

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

Дисковая квота кончилась при отладке композита.

Перед каждой перезаписью платформа сохраняет прежнюю версию копии отдельным файлом с меткой времени. При частой пересборке такие файлы накапливаются пачками и съедают место незаметно.

Копия переписывается на каждом обращении к странице.

В разметку попало значение, своё у каждого посетителя: идентификатор сессии, гостевой номер или случайная строка. Контрольная сумма расходится всякий раз, и платформа честно пересобирает копию заново.

Вёрстка развалилась сразу после включения композита.

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

Частые вопросы

Композитный сайт и ускорение сайта - это одно и то же?

Нет, это разные технологии. Композит хранит готовый HTML страницы на своём сервере и ускоряет только время ответа.

Чем композит отличается от кэша компонента?

Кэш компонента хранит результат одного вызова, а страницу вокруг всё равно собирает платформа. Композит отдаёт готовую страницу целиком.

Почему ответ «не изменилось» приходит только с третьего раза?

Первое обращение создаёт копию, второе отдаёт дату изменения, третье присылает её обратно. Это штатный порядок, а не сбой.

Как понять, какой компонент голосует против?

По отладочной копии страницы рядом с файлом кеша. В ней перечислены голоса, и виновник виден по имени компонента.

Защитит ли композит закрытый раздел от гостей?

Нет, копия отдаётся без проверки прав доступа. Закрытые разделы выводят из композита масками исключений в настройках модуля.

Смежное

Первоисточник