SSR и все с ним связанное

Рендеринг на клиентской стороне или не сервере? И там, и там!

В сообществе ведутся споры о том как правильно использовать серверный рендеринг.

Идеально было бы использовать что-то вроде прогрессивной загрузки: пререндерить контент на сервере чтобы улучшить First Contenful Paint (FCP), но при этом загрузить минимально необходимый JavaScript, чтобы получить хороший показатель TTI (time to interactive). Если загрузить JavaScript слишком поздно, вы заблокируете основной поток во время парсинга, компиляции и выполнения, тем самым ограничивая интерактивность вашего приложения.

Чтобы избежать этого, всегда разбивайте ваши функций на отдельные асинхронные части и, по возможности, используйте requestIdleCallback. Загружайте асеты лениво используя динамический import () WebPack’а. Так вы сможете отложить парсинг и компиляцию до тех пор, пока ваши асеты действительно не понадобятся пользователям.

Метрика Time to Interactive (TTI) время до момента когда страница становится полностью интерактивной. Это время определяется так:
  • начинаем отсчитывать от момента FCP
  • ждем 5 секунд, если за эти 5 секунд были таски, время которых превысило 50 ms, начинаем измерять сначала
Как только наша страница стала интерактивной мы можем по требованию загружать остальные менее критичные части нашего приложения.

К сожалению, как заметил Пол Льюис прогрессивную загрузку достаточно сложно реализовать если имеешь дело с фреймворками.

На сегодняшний день есть несколько вариантов решения этой проблемы. Отличный обзор этих решений представлен в докладе Rendering on the Web и в статье Modern Front-End Architectures. Приведённый ниже обзор основан на этих двух источниках.

Full Server-Side Rendering (SSR)

В классическом SSR (например, WordPress), все запросы обрабатываются исключительно на сервере. Запрашиваемый контент возвращается в виде готовой HTML-страницы, и браузеры могут сразу же ее отобразить. Промежуток между FCP и TTI обычно невелик, и страница рендерится сразу же, как HTML передается в браузер.

Это позволяет избежать дополнительных запросов для загрузки данных. Все обрабатывается до того, как браузер получит ответ. Однако в итоге наш сервер больше «думает», что увеличивает Time To First Byte. Кроме этого мы не можем использовать все возможности современных приложений.

Статический рендеринг

SPA где все страницы предварительно рендерятся в статический HTML с минимальным количеством JavaScript. Это происходит на этапе сборки. Это означает, что при статическом рендеринге мы заранее создаем отдельные HTML-файлы для каждого возможного URL. Не многие приложения могут себе такое позволить. Но поскольку HTML для страницы не нужно генерировать на лету, можно добиться стабильно быстрого Time To First Byte. Таким образом, можно быстро отобразить целевую страницу, а затем выполнить префетчинг SPA-фреймворка для последующих страниц. Применив такой подход, Netflix сократил загрузку и TTI на 50%.

SSR c (ре)гидрацией (Universal Rendering, SSR + CSR)

А что если взять лучшее от двух подходов — SSR и CSR? При использовании гидрации возвращаемая с сервера HTML-страница также содержит скрипт, который загружает полноценное приложение на стороне клиента. В идеале мы достигаем быстрого FCP (как в SSR), а затем продолжаем рендеринг с (ре)гидрацией. К сожалению, так бывает редко. Чаще всего пользователь видит полностью загруженну, но не интерактивную страницу.

В React можно использовать модуль ReactDOMServer в Node (например Express), а затем вызвать метод renderToString для рендеринга top-level компонентов в виде статического HTML. Во Vue. js для этих целей используется используется vue-server-renderer. В Angular — @nguniversal.
Full SSR работает из коробки в Next.js (React) или Nuxt.js (Vue).

У этого подхода есть недостатки. Мы получаем полную гибкость на клиенте, но большую задержку между FCP и TTI, а увеличенный First Input Delay. Rehydration слишком дорогой процесс и может сильно увеличить TTI.

Стриминг SSR + CSR

Чтобы минимизировать разрыв между TTI и FCP, мы выполняем несколько запросов одновременно и отправляем результат чанками, по мере их генерации. Мы оправляем данные в процессе генерации, а не ждем когда сгенерируется весь HTML. Такой подход позволяет улучшить Time To First Byte.
Чтобы реализовать такой подход в React вместо renderToString () мы можем использовать renderToNodeStream () Также мы React Suspense мы можем использовать асинхронный рендеринг и для этой цели.
Во Vue мы можем использовать renderToStream ().

На клиенте, вместо того чтобы загружать все приложение сразу, мы можем загружать компоненты постепенно. Сначала приложение разбивается на части, каждой из которых соответствует свой скрипт. Эти скрипты генерируются при помощи code splitting. И потом каждая гидрируется (hydrate) отдельно. Сначала мы гидрируем самые критичные части. Еще мы можем отложить гидрацию до тех пор, пока компонент не появится во вьюпорте. Или до момента когда основной поток будет не занят

Для Vue Маркус Оберленер (Markus Oberlehner) опубликовал руководство по уменьшению TTI в SSR-приложениях с помощью гидрации на user-interaction, а также vue-lazy-hydration — плагин, который позволяет гидратировать компоненты по мере изменения их видимости или при определенных видах взаимодействия пользователя с компонентом. Команда Angular работает над прогрессивной гидрацией с помощью Ivy Universal. Еще вы можете реализовать частичную гидрацию с помощью Preact и Next. js.

Trisomorphic Rendering

Вы можете использовать потоковый рендеринг (streaming rendering) для начальной навигации (той что происходит без помощи JS) а затем вы можете переключаться на service workers которые после установки будут перерндерить контент. Так вы получите SPA-подобную навигацию. Такой подход хорошо работает если вы шарите код для роутинга между сервером, клиентом и сервисе воркером.

И всё же — SSR или CSR?

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

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

Стримьте ваш HTML и используйте прогрессивную гидрацию.
AirBnB экспериментирует с прогрессивной гидрацией; они откладывают загрузку ненужных компонентов, загружают при взаимодействии с пользователем (прокрутка) или во время простоя.