Catberry.js: Прогрессивный рендеринг

image

В предыдущей статье я рассказывал про реализацию Flux и веб-компонентов во фреймворке Catberry.js, и эта статья — обещанное продолжение про движок прогрессивного рендеринга.


Наверное, вы сталкивались хотя бы раз в жизни с JPEG-картинкой, которая сперва загружается с сервера мутной, а по мере загрузки остального содержимого становится чётче. Такой формат картинки называется «Progressive JPEG», и его основная идея — показать как можно скорее пользователю хоть какое-то содержимое, пусть и не до конца готовое. Пользователь с первой секунды уже будет знать размер картинки и ее примерное содержимое, а в дальнейшем содержимое будет становится только отчётливее.

Лично я не знаю откуда появилась идея назвать потоковый (stream-based) рендеринг HTML прогрессивным, но первое применение этого термина именно к рендерингу HTML я нашел в статье за декабрь 2009 года «Progressive rendering via multiple flushes». Однако и в более свежих статьях, например от разработчиков Google, можно найти термин «Optimized (progressive) rendering».


Что обычно используется в других решениях?


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

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

Как бы вы отнеслись к тому, что видите около 5 секунд такую ситуацию? Лично я думаю, что вот-вот увижу »504 Gateway Timeout» или нечто подобное и закрываю вкладку браузера. Ведь абсолютно неясно, обрабатывается ли ваш запрос на сервере или он просто повис. Может быть сайт вообще под DDoS-атакой и не стоит на него заходить.

Прогрессивный подход


Прогрессивный рендеринг же использует «Chunked transfer encoding», который появился в спецификации HTTP 1.1. Это кодирование содержимого позволяет отдавать контент страницы порциями через специальные маркеры, не указывая заголовок Content-Length. Этот подход из коробки поддерживается Node.js через его Stream API, что упрощает реализацию на этой платформе.

Таким образом, Catberry.js запрашивает данные не для всей страницы, а по веб-компонентам и отдаёт порции HTML в браузер настолько быстро, насколько готовы данные и шаблон каждого веб-компонента.

Другими словами, Catberry.js сразу же начинает отдавать корневой шаблон страницы в браузер через реализованный Readable Stream. Когда внутри него встречается веб-компонент, он ставит поток данных на паузу, запрашивает данные для этого веб-компонента и после их получения двигается дальше, пропуская отрендеренный шаблон веб-компонента через такой же механизм поиска уже вложенных веб-компонентов. В результате, пользователь в браузере видит мгновенное появление частей страницы, которые вообще не требуют запросов данных. Части страницы, которые требуют данных, появляются с задержкой, равной длительности запроса данных конкретно для этой части, а не для всей страницы целиком.

Я считаю, что схема от разработчиков Google из упомянутой статьи лучше всего описывает саму суть прогрессивного рендеринга:

e78ef8d311534bbdb55b908f23429887.png


В случае прогрессивного рендеринга, пользователь с первых миллисекунд знает, что ваш сайт отвечает, его запрос обрабатывается и ему будет казаться, что ваш сайт невероятно быстр и отзывчив. К тому же, когда вы мгновенно отдаете части страницы со скриптами или стилями, то браузер начинает их загружать параллельно основному документу HTML. Пока ваш сервер запрашивает данные для веб-компонента, браузер уже загрузит стили и скрипты. Как демонстрация — сетевая диаграмма запросов раздела документации с официального сайта Catberry.js.

5f1100983e5a42b0acbaa335948e99e7.png


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

Недостатки?


Как и любой другой подход, этот тоже не идеален.

Как только мы отправили первый байт HTML в браузер, мы отправляем и HTTP-заголовки, после чего мы больше не имеем возможности их выставлять. Любопытно, что для решения как раз этой проблемы спецификацией HTTP 1.1 предусмотрены HTTP Trailers, которые работают аналогично заголовкам HTTP, но приходят клиенту в конце потока данных, а не в начале. Изначально я планировал использовать как раз этот механизм для управления заголовками в Catberry.js, но меня постигло разочарование. Не смотря на то, что Node.js прекрасно поддерживает трейлеры, эксперименты показали, что браузеры и не думали это поддерживать — трейлеры просто игнорируются.

Как же нам выставить Cookie или сделать редирект в Catberry.js, спросите вы?

Как вы могли прочитать в предыдущей статье, в Catberry.js есть специальный веб-компонент «head», который работает с -элементом страницы. Если до момента рендеринга HTML этого компонента вы используете методы:

  • this.$context.notFound() — передаёт управление следующему Express middleware
  • this.$context.redirect('/some/path')
  • this.$context.cookie.set({key: 'hello', value: 'world'})


То произойдёт честный редирект через HTTP-код ответа 302 и заголовок Location, а в случае с Cookie, будет заголовок Set-Cookie. Другими словами, пока ваш метод «render» компонента «head» не вернёт данные, все эти методы выставят нужный HTTP-код ответа и заголовки.

Если вызывать эти методы позже, в начале веб-компонента, где они были вызваны, будет добавлен