Ленивый строитель

Ленивый строитель (англ. «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).

Предупреждение

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

create_placeholder

Логическое свойство #create_placeholder отвечает за то, будет ли создан заполнитель для текущего ленивого строителя.

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

При отсутствии данного значения, Drupal предпримет попытку автоматического определения, требуется ли использовать ленивый вызов и добавлять заполнитель. Данное определение происходит на основе кеш-метаданных (#cache) переданных в рендер массиве ленивого строителя и настройки renderer.config:auto_placeholder_conditions.

Совет

Вы можете менять данные настройки в sites/default/default.services.yml файле или иметь разные для разных окружений.

По умолчанию данные настройки имеют следующие значения:

  • 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 ничего не знает о таком формате заполнителя. Для подобного подхода требуется наличие стратегии заполнителя, которая понимает подобный формат.

Изменения в релизах

  • Drupal 9.2.0 (02.06.2021): Система рендера теперь выбрасывает исключение, если в качестве результата функции обратного вызова возвращается что-то отличное от рендер массива.

Ссылки

💝 Помогите нам сделать документацию лучше!

Вся документация Druki с отрытым исходным кодом. Нашли ошибку или неточность? Создайте pull request.

Редактировать текущий документ Обсудить улучшение

Или узнайте как контрибутить.

🤔 По-прежнему нужна помощь?

Не нашли ответа на свой вопрос? Попросите помощи у сообщества!

Задайте вопрос на GitHub Смотрите другие ресурсы сообщества