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

Недавно я обнаружил одно интересное применение стриминга, о котором раньше и не подозревал.

В чем проблема?

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

Наивное решение по умолчанию

Для приложения рендеринга на стороне клиента мы могли бы переместить все вызовы API непосредственно в клиент, чтобы следующие 2 шага отображались в последовательном порядке для каждой загрузки страницы:
Шаг 1. скачать файлы JS/CSS
Шаг 2. когда они будут готовы, выполните необходимые вызовы API из клиента

TTI = задержка + время (шаг 1) + задержка + время (шаг 2) + рендеринг

Желаемое решение

Я хотел избежать этой двойной задержки и даже использовать распараллеливание. По сути, мы должны иметь возможность загружать файлы JS/CSS параллельно с вызовом API, сохраняя рендеринг на стороне клиента. TTI может быть значительно увеличен, если мы активируем это и будем измерять следующим образом:

TTI = Задержка + МАКС(Время(Шаг 1), Время(Шаг 2))+ Рендеринг

Алгоритм следующий:

  1. Начать потоковую передачу HTML
    В начале потока HTML должны быть теги
    <script defer src="script.js"> и <link href="styles.css">, чтобы браузер начал загрузку ресурсов как можно скорее.
    Отложитьатрибут важен, чтобы не останавливать синтаксический анализ HTML и выполнять скрипты только после того, как страница будет полностью отрисована при сохранении порядка.
  2. Преобразование HTML на лету
    Затем бессерверная функция должна передавать данные, поступающие от API, непосредственно в HTML.
    В случае ответа JSON данные могут быть внедрено в теги <script type="application/json">.
  3. Закрыть поток
    Когда поток закрыт, DOMContentLoaded будет запущен, и загруженные файлы JS/CSS начнут рендеринг.

В этом решении мы добились желаемого параллелизма, сохраняя при этом рендеринг на стороне клиента.

Бессерверная функция

Самая сложная часть здесь — код бессерверной функции. По сути, функция должна поддерживать следующее:

  • Перехватывать все запросы к странице
  • Делайте вызовы API параллельно и передавайте их обратно в HTML

Наш выбор пал на Cloudflare@worker с его замечательным HTMLRewriter API. Это позволяет вам асинхронно изменять HTML, а затем передавать его обратно.

addEventListener('fetch', async (event: FetchEvent) => {
  const request = await fetch(event.request);
  const response = new HTMLRewriter()
    .on('#config-1', new Config1())
    .on('#config-1', new Config2())
    .transform(request);
  event.respondWith(response);
});

Хотя AWS@lambda и AWS@edge не имеют встроенной поддержки потоковой передачи, ее все же можно реализовать с помощью собственного JS.

Это решение было протестировано в продакшне, и прирост производительности был очень заметен, он выглядел даже лучше, чем то, что мы имели с SSR.