Ленивый строитель (англ. «Lazy Builder») — API, позволяющий отложить обработку медленного кода не блокируя основной поток.
¶Введение
PHP — не асинхронный язык, это означает что вся обработка производится последовательно и медленный код тормозит весь процесс.
Медленный код сильно сказывается на общей производительности сайта. Обычно подобные проблемы решаются кешированием, но бывает, что подобные вычисления нужно производить в рантайме или каждый раз выдавать новые данные, кеш тут уже не поможет, зато помогут ленивые строители.
Например, мы имеем следующий код:
function foo_a(): void {
echo '<p>Hello World!</p>';
}
function foo_b(): void {
sleep(1000);
echo '<p>Foo</p>';
}
function foo_c(): void {
echo '<p>Bar</p>';
}
foo_a();
foo_b();
foo_c();
Результатом выполнения будет:
<p>Hello World!</p>
<p>Foo</p>
<p>Bar</p>
Данный код будет выполняться минимум 1 секунду, так как в foo_b()
находится sleep()
на такой промежуток.
Ленивый строитель позволят объяснить Drupal что мы знаем о медленном коде или тех местах, где он может появиться. В нашем случае мы точно знаем что это foo_b()
.
Пример, если бы мы встроили туда ленивый строитель.
function foo_a(): void {
echo '<p>Hello World!</p>';
}
function foo_b(): array {
return [
'#lazy_builder' => ['foo_b_lazy', []],
];
}
function foo_b_lazy(): string {
sleep(1000);
return '<p>Foo</p>';
}
function foo_c(): void {
echo '<p>Bar</p>';
}
foo_a();
foo_b();
foo_c();
При подобном коде, Drupal бы прежде всего сформировал следующий результат (примерно):
<p>Hello World!</p>
<drupal-render-placeholder callback="foo_b_lazy" arguments="" token="TOKEN"></drupal-render-placeholder>
<p>Bar</p>
На текущем этапе код отработает настолько быстро, насколько это возможно, потому что foo_b()
теперь отдаёт ленивый строитель, а Drupal заменяет его на заполнитель <drupal-render-placeholder>
.
По умолчанию, Drupal, в конце обработки запроса, перед самым ответом заменит все подобные заполнители реальными значениями и итоговый результат будет:
<p>Hello World!</p>
<p>Foo</p>
<p>Bar</p>
Мы получим точно такой же результат, но код выполняется уже в другом порядке. Да, «от перестановки слагаемых сумма не меняется» и код по-прежнему будет обрабатываться минимум 1 секунду.
Ленивые строители отвечают только за подготовку разметки для медленного кода, затем обработка передаётся стратегиям заполнителей — которые и решают что делать с каждым конкретным заполнителем типа <drupal-render-placeholder>
.
Drupal также поставляет модуль BigPipe, который предоставляет новую стратегию, основанную на одноимённом подходе Facebook. В случае примера выше, пользователь получит ответ до того как <drupal-render-placeholder>
будет заменён на результат. После того как ответ будет отправлен, Drupal дождётся завершения foo_b_lazy()
и заменит заполнитель на странице пользователя без перезагрузок и дополнительных запросов. TTFB в таком случае будет равен тому, сколько нужно PHP на вызов foo_a()
и foo_c()
(<1ms), но итоговое время загрузки будет по-прежнему минимум секунду. Смотрите пример для визуального представления.
Вы можете создавать свои стратегии и подходы и комбинировать их для одной страницы.
¶Структура ленивого строителя
Ленивый строитель — рендер массив с ключом #lazy_builder
при помощи которого передаётся информация о том, как необходимо обрабатывать его и что возвращает значение. По сути, мы указываем функцию обратного вызова и аргументы для неё, которую необходимо вызывать, когда Drupal вызовет обработку данного кода.
Рендер массив с описанием ленивого строителя может иметь только следующие свойства:
-
#lazy_builder
: Массив описывающий ленивый строитель. -
#create_placeholder
: Принимает логическое значения, необходимо ли создавать заполнитель для текущего ленивого строителя. По умолчанию отключено. -
#cache
: Для передачи кеш метаданных. -
#attached
: Для подключения библиотек или более комплексного ленивого строителя. -
#weight
: Вес элемента, если это требуется, например в Form API.
Данный рендер массив не может содержать никаких иных свойств и вложенных массивов. Нарушение данного правила приведёт к вызову исключения.
¶lazy_builder
Свойство #lazy_builder
— массив, содержащий массив с функцией обратного вызова и массивом из аргументов, которые необходимо передать функции обратного вызова.
Пример:
$build = [
'#lazy_builder' => [
'Foo::bar', ['Hello', 'World'],
],
];
Подобная запись равняется Foo::bar('Hello', 'World')
.
Функцией обратного вызова может быть:
- PHP функции обратного вызова (callable).
- Сервисы в формате
{service_name}:{method}
.
Аргументами для функции обратного вызова могут быть только скалярные значения (int
, float
, string
, bool
, NULL
).
¶create_placeholder
Логическое свойство #create_placeholder
отвечает за то, будет ли создан заполнитель для текущего ленивого строителя.
По умолчанию, ленивый строитель не создаёт заполнитель и сразу выполняет код. Это сделано для того, чтобы вы могли размечать потенциально медленные части кода, где неизвестно, будет ли медленный код или нет.
При отсутствии данного значения, Drupal предпримет попытку автоматического определения, требуется ли использовать ленивый вызов и добавлять заполнитель. Данное определение происходит на основе кеш-метаданных (#cache
) переданных в рендер массиве ленивого строителя и настройки renderer.config:auto_placeholder_conditions
.
По умолчанию данные настройки имеют следующие значения:
-
max-age
равный0
: Заполнитель будет создаваться еслиmax-age
меньше или равен данному значению. -
contexts
равныйsession
иuser
: Заполнитель будет создаваться если в кеш-метаданных есть один из этих кеш-контекстов. -
tags
с пустым массивом: Заполнители по умолчанию не реагирую на кеш-теги.
Если вы хотите чтобы ваш ленивый строитель всегда работал, укажите #create_placeholder
равным TRUE
, в таком случае все условия и автоматизация будет пропущена и рендер массив гарантированно будет обработан лениво.
Пример:
$build = [
'#create_placeholder' => TRUE,
'#lazy_builder' => [
'Foo::bar', ['Hello', 'World'],
],
];
¶attached
Свойство #attached
также может быть использовано для создания ленивых строителей. На самом деле, именно так информация о ленивых строителях и передается если вы будете использовать #lazy_builder
и #create_placeholder
.
Данный подход будет полезен в тех случаях, когда вы хотите влиять на то, какой должен быть заполнитель, особенно в случаях, когда вы хотите использовать разные стратегии заполнителей на одной странице, или отделять свои стратегии от стандартных.
При данном подходе, информация о ленивых строителях передаётся через ключ массива placeholders
, который может содержать массивы где:
- Ключ массива — фактический заполнитель.
- Значение — массив с описанием ленивого строителя.
Пример:
$build = [
'#markup' => 'Hello <span class="my-placeholder"></span> World!',
'#attached' => [
'placeholders' => [
'<span class="my-placeholder"></span>' => [
'#lazy_builder' => [
'Foo::bar', ['Hello', 'World'],
],
],
],
],
];
¶Изменения в релизах
- Drupal 9.2.0 (02.06.2021): Система рендера теперь выбрасывает исключение, если в качестве результата функции обратного вызова возвращается что-то отличное от рендер массива.
¶Ссылки
- Drupal 8: #lazy_builder — ленивый билдер, Niklan, 2017
- Плагин блока, генерящий динамический контент с помощью #lazy_builder, xandeadx, 2021