Как разработчики, мы часто сталкиваемся с решениями, которые влияют на всю архитектуру наших приложений. Одно из основных решений, которое должны принять веб-разработчики - это где реализовывать логику и рендеринг в своем приложении. Это может быть непросто, так как существует множество различных вариантов построения сайта.


о переводе

Переведя почти всю статью я внезапно обнаружил, что в том же источнике она присутствует и на русском языке (сразу не обнаружил потому что в выпадающем списке переводов нет русского языка, перевод нашелся через Google - https://developers.google.com/web/updates/2019/02/rendering-on-the-web?hl=ru). Однако, при ближайшем рассмотрении перевод "рендеринг сервера" вместо "статический рендеринг" сразу дал понять что перевод был сделан машиной.
Непосредственно перед публикацией своего перевода здесь, я ещё раз зашел на тот русский перевод и ... обнаружил что его кто-то уже существенно улучшил :))) Однако, на мой пристальный взгляд, он всё равно не лишён недостатков.
В итоге, я всё же решил опубликовать свой вариант перевода, так как, на мой взгляд, статья в любом случае достойна того чтобы привлечь к ней внимание, а какой перевод прочесть, в итоге каждый выберет сам.
Спасибо!

Наше понимание в этой области основано на нашей работе с Chrome, и контактировании с большими сайтами в течение последних нескольких лет. В общем, мы хотим вдохновить разработчиков рассмотреть использование серверного рендеринга или статического рендеринга с полноценной регидратацией.

Чтобы лучше понимать архитектуры, из которых мы выбираем, когда принимаем решение, нам необходимо иметь четкое понимание каждого подхода и последовательную терминологию, которую мы будем использовать, когда говорим о них. Различия между этими подходами помогают проиллюстрировать компромиссы при рендеринге в вебе через призму производительности.

Терминология

Рендеринг

  • SSR: Server-Side Rendering - рендеринг в HTML клиентского или универсального приложения на сервере.

  • CSR: Client-Side Rendering - рендеринг приложения в браузере, обычно используя DOM

  • Rehydration (регидратация): "загрузка" JavaScript отображениий на клиенте таким образом, чтобы они повторно использовали отрендеренное на сервере DOM-дерево и данные HTML-а

  • Prerendering (пре-рендеринг): выполнение клиентского приложения во время сборки для захвата его начального состояния в виде статического HTML.

Performance

  • TTFB: Time to First Byte - время между нажатием на ссылку и временем прихода первого бита контента

  • FP: First Paint - время когда первый пиксель становится виден пользователю

  • FCP: First Contentful Paint - время до показа пользователю запрошенного контента (тела статьи и т.п.)

  • TTI: Time To Interactive - время до момента когда страница становится интерактивной (начинают работать события и т.д.)

Server Rendering (Серверный рендеринг)

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

Серверный рендеринг обычно даёт быстрый First Paint (FP) и First Contentful Paint (FCP). Выполнение логики страницы и её рендеринг на сервере позволяют избежать отправки большого количества JavaScript клиенту, что помогает достичь быстрого Time to Interactive (TTI). Это имеет смысл потому, что при серверном рендеринге вы на самом деле просто посылаете текст и ссылки в браузер пользователя. Такой подход может хорошо работать для широкого спектра устройств и сетевых условий и открывает интересные возможности для оптимизации браузера, например можно выполнять разбор потоковых (streaming) документов.

При серверном рендеринге пользователи вряд ли будут вынуждены ждать, пока CPU-зависимый JavaScript будет выполнен, прежде чем они смогут использовать ваш сайт. Даже когда стороннего JS не избежать, использование серверного рендеринга для уменьшения собственных JS costs (JS затрат) может дать вам больше "budget" (бюжета) для остального. Однако, есть один основной недостаток такого подхода: генерация страниц на сервере занимает время, что часто может привести к замедлению Time to First Byte (TTFB).

Достаточно ли серверного рендеринга для вашего приложения, во многом зависит от того, какое приложение вы строите. Существует давняя дискуссия о правильности применения серверного рендеринга вместо клиентского рендеринга, но важно помнить, что вы можете использовать серверный рендеринг для одних страниц, а для других нет. Некоторые сайты с успехом переняли гибридный рендеринг. Netflix делает серверный рендеринг своих относительно статических страниц, в то время как делает prefetching JS для страниц с тяжелым взаимодействием, давая этим более тяжелым отрендеренным на клиенте страницам больше шансов на быструю загрузку.

Многие современные фреймворки, библиотеки и архитектуры позволяют отрисовывать одно и то же приложение как на клиенте, так и на сервере. Эти инструменты могут быть использованы для Server Rendering, однако важно отметить, что архитектуры, где рендеринг происходит как на сервере, так и на клиенте, являются собственным классом решений с очень различными характеристиками производительности и компромисами. React пользователи могут использовать для серверного рендеринга renderToString() или решения, построенные на нем, такие как Next.js. Пользователи Vue могут ознакомиться с руководством по серверному рендерингу Vue или познакомиться с Nuxt. В Angular есть Universal. Однако большинство популярных решений используют ту или иную форму гидратации (hydration), поэтому перед выбором инструмента следует ознакомиться с используемыми подходами.

Static Rendering (Статический рендеринг)

Статический рендеринг происходит во время сборки и даёт быстрый FP, FCP и TTI - это если предопложить, что количество клиентского JS невелико. В отличие от серверного рендеринга, ему также удаётся достичь стабильно быстрого TTFB, так как HTML для страницы не нужно генерировать "на лету". Как правило, статический рендеринг означает создание отдельного HTML-файла для каждого URL заранее. С HTML генерируемым заранее, статический рендеринг может быть развернут на нескольких CDN, чтобы воспользоваться преимуществами edge-кеширования.

Решения для статического рендеринга бывают разных форм и размеров. Такие инструменты как Gatsby разработаны для того, чтобы разработчики чувствовали, что их приложение отрисовывается динамически, а не генерируется на этапе сборки. Другие, такие как Jekyll и Metalsmith, принимают их статическую природу, предоставляя подход более заточенный на шаблоны.

Одним из недостатков статического рендеринга является то, что отдельные HTML-файлы должны быть сгенерированы для каждого возможного URL. Это может быть сложно или даже невозможно, когда вы не можете предсказать, какими будут эти URL заранее, или если на сайте большое количество уникальных страниц.

React пользователи могут быть знакомы с Gatsby, Next.js static export или Navi - все они дают удобство для использующих эти компоненты. Однако, важно понимать разницу между статическим рендерингом и пре-рендингом: статический рендеринг страниц интерактивен без необходимости выполнения большого количества клиентского JS, в то время как пре-рендеринг улучшает FP (First Paint) или FCP (First Contentful Paint) одностраничного приложения (SPA), которое должно быть загружено на клиенте для того, чтобы страницы были по-настоящему интерактивными.

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

Другой полезный тест - замедление работы сети с помощью Chrome DevTools и наблюдение за тем, сколько JavaScript было загружено до того, как страница стала интерактивной. Пре-рендеринг обычно требует больше JavaScript для получения интерактивности, а так же этот JavaScript имеет тенденцию быть более сложным, чем Progressive Enhancement подход, используемый при статическом рендеринге.

Серверный рендеринг против статического

Серверный рендеринг не является серебряной пулей - его динамическая природа может сопровождаться значительными накладными расходами. Многие решения для серверного рендеринга don't flush early, могут задерживать TTFB или задваивать отправленные данные (например, в inlane стэйте, используемом JS на клиенте). В React, renderToString() может быть медленным, так как он синхронный и однопоточный. Получение "правильного" рендеринга сервера может включать в себя поиск или создание решения для компонентного кеширования, управление потреблением памяти, применение memoization техник, и многие другие вопросы. Как правило, вы обрабатываете/пересобираете одно и то же приложение несколько раз - один раз на клиенте и один раз на сервере. То, что серверный рендеринг может заставить что-то появиться раньше, не означает, что у вас вдруг стало меньше работы.

Серверный рендеринг генерирует HTML по требованию для каждого URL, но это может быть медленнее, чем просто обслуживание статически отрендереного контента. Если вы готовы сделать дополнительные усилия, то серверный рендеринг + [HTML кеширование] (https://freecontent.manning.com/caching-in-react/) может значительно сократить время серверного рендеринга. Положительной стороной серверного рендеринга является возможность получать более "живые" данные и отвечать на более полный набор запросов, чем это возможно при статическом рендеринге. Страницы, требующие персонализации, являются хорошим примером типа запроса, который плохо работает со статическим рендерингом.

Серверный рендеринг также может представлять интересные решения при построении PWA. Лучше ли использовать full-page service worker кеширование, или просто рендерить на сервере отдельные фрагменты контента?

Client-Side Rendering (CSR)

Рендеринг на стороне клиента (CSR) означает рендеринг страниц непосредственно в браузере с использованием JavaScript. Вся логика, сбор данных, шаблонирование и маршрутизация обрабатываются на клиенте, а не на сервере.

Клиентский рендеринг может быть сложным в части получения и быстроты на мобильных устройствах. Он может достигать производительности чистого сервер-рендеринга, если делать минимальную работу, сохраняя компактным JavaScript бюджет и доставляя объёмы в как можно меньшем количестве RTTs. Критические скрипты и данные могут быть доставлены быстрее с помощью HTTP/2 Server Push или <link rel=preload>, что заставит парсер работать на вас быстрее. Такие шаблоны, как PRPL, стоит рассмотреть, чтобы первоначальная и последующая навигация чувствовалась быстрыми.

Основным недостатком Client-Side Rendering является то, что количество требуемого JavaScript имеет тенденцию расти по мере роста приложения. Это становится особенно трудным с добавлением новых JavaScript-библиотек, полифилов и стороннего кода, которые конкурируют за вычислительную мощность и часто должны быть обработаны, прежде чем содержимое страницы может быть визуализировано. Опыт построения CSR, опирающийся на большие пакеты JavaScript, должен учитывать агрессивное разделение кода, и чувствовать себя уверенным в работе с ленивой загрузкой JavaScript - "обслуживать только то, что вам нужно и когда вам это нужно". Для случаев с небольшой интерактивностью или вообще без нее, серверный рендеринг может представлять собой более масштабируемое решение этих проблем.

Для тех, кто создает одностраничное приложение, определение основных частей пользовательского интерфейса, разделяемого большинством страниц, означает возможность применить технику Application Shell caching. В сочетании с service workers это может драматически повысить воспринимаемую производительность при повторных визитах.

Комбинация серверного рендеринга и клиентского через регидратацию

Часто называемый Universal Rendering или просто "SSR", этот подход пытается сгладить компромиссы клиентского и серверного редеринга, делая и то, и другое. Навигационные запросы, такие как полная загрузка страницы или перезагрузка, обрабатываются сервером, который рендерит приложение в HTML, затем JavaScript и данные, используемые для рендеринга, встраиваются в результирующий документ. При тщательной реализации, это даёт быстрый FCP (First Contentful Paint) такой же, как Server Rendering, а далее "усиливает это" путем рендеринга опять же на клиенте с помощью техники, называемой (re)hydration ((ре)гидратация). Это новое решение, но оно может иметь некоторые существенные недостатки в производительности.

Основной недостаток SSR с регидратацией (rehydration) заключается в том, что она может оказать значительное негативное влияние на TTI (Time To Interactive), даже если она улучшает FP (First Paint). SSR-страницы часто выглядят обманчиво полностью загруженными и интерактивными, но на самом деле не могут реагировать на ввод, пока не будет выполнен JS на стороне клиента и не будут прикреплены обработчики событий. Это может занять секунды или даже минуты на мобильном устройстве.

Возможно, вы сами это испытавали - в течение некоторого времени после того, как страница выглядит так, как будто она загружена, нажатие на нее или касание ничего не даёт. Это быстро расстраивает... Почему ничего не происходит? Почему я не могу скроллить?

= Проблема регидратации: Одно приложение по цене двух

Проблемы с регидратацией часто могут быть хуже, чем задержка интерактивности из-за JS. Для того, чтобы JavaScript на стороне клиента мог точно "определить" ("pick up") то место, где остановился сервер, без необходимости повторно запрашивать все данные, использованные сервером для рендеринга этого HTML, текущие SSR решения обычно сериализуют ответ из зависимых данных UI в документ в виде тегов script. Полученный HTML-документ содержит высокий уровень дублирования:

Как вы видите, сервер возвращает описание пользовательского интерфейса приложения в ответ на навигационный запрос, но также возвращает исходные данные, использованные для составления этого интерфейса, и полную копию реализации интерфейса, которая затем загружается на клиенте. Только после того, как bundle.js завершит загрузку и выполнение, этот пользовательский интерфейс станет интерактивным.

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

Но всё же надежда на SSR с регидратацией есть. В краткосрочной перспективе, только использование SSR для высоко кешируемого содержимого может уменьшить задержку TTFB (Time to First Byte), давая результаты, схожие с пре-рендерингом. Регидратация инкрементальная, прогрессивная или частичная, может быть ключом к тому, чтобы сделать эту технику более жизнеспособной в будущем.

Потоковый серверный рендеринг и прогрессивная регидратация

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

Потоковый серверный рендеринг (Streaming server rendering) позволяет посылать HTML в чанках, которые браузер может прогрессивно рендерить по мере получения. Это может обеспечить быстрый FP (First Paint) и FCP (First Contentful Paint), так как разметка поступает к пользователям быстрее. В React, потоковость, будучи асинхронной в renderToNodeStream() - по сравнению с синхронным renderToString - означает, что backpressure обрабатывается хорошо.

Прогрессивная регидратация также заслуживает внимания, и кое-что в React было исследовано. При таком подходе отдельные части приложения, возвращаемого с сервера, "загружаются" постепенно, вместо текущего общепринятого подхода когда инициализируется сразу всё приложение. Это может помочь уменьшить количество JavaScript, необходимого для того, чтобы сделать страницы интерактивными, так как обновление на клиентской стороне низкоприоритетных частей страницы может быть отложено, чтобы предотвратить блокировку основного потока. Это также может помочь избежать одной из наиболее распространенных ловушек SSR Rehydration, когда отрендеренный на сервере DOM разрушается, а затем сразу же восстанавливается - чаще всего потому, что начальный синхронный рендеринг на стороне клиента требует данных, которые были не совсем готовы, возможно, ожидая завершения Promise.

= Частичная регидратация

Частичная регидратация оказалась трудной для осуществления. Этот подход является продолжением идеи прогрессивной регидратации, когда отдельные части (компоненты / виджеты / деревья), подлежащие прогрессивной регидратации, анализируются, а те, которые обладают низкой интерактивностью или не обладают реактивностью помечаются. Для каждой из этих наиболее статических частей соответствующий код JavaScript затем трансформируется в инертные ссылки и декоративную функциональность, уменьшая их влияние на стороне клиента до почти нулевого уровня. Подход, основанный на частичной гидратации, имеет свои собственные проблемы и компромиссы. Он создает некоторые интересные вызовы для кеширования, а навигация на стороне клиента означает, что мы не можем иметь HTML рендерящийся на сервере для инертных частей приложения и доступный без полной загрузки страницы.

= Трисоморфный рендеринг (Trisomorphic Rendering)

Если service workers, являются подходящим вариантом для вас, то "трисоморфный" рендеринг также может быть вам интересен. Это метод, при котором вы можете использовать потоковый серверный рендеринг для начальных/не-JS навигаций, а затем попросить ваш service worker взять на себя рендеринг HTML для навигации после того как он будет смонтирован. Это может поддерживать кешированные компоненты и шаблоны в актуальном состоянии и позволяет использовать навигацию в стиле SPA для рендеринга новых UI-частей в той же сессии. Такой подход лучше всего работает, когда вы можете поделиться одним и тем же шаблоном и кодом маршрутизации между сервером, клиентской страницей и service worker.

SEO соображения

Команды часто учитывают влияние SEO при выборе стратегии для рендеринга в вебе. Серверный рендеринг часто выбирается для обеспечения поисковым роботам возможности лёгкого "полного поиска". Поисковые роботы могут понимать JavaScript, но часто существуют ограничения, о которых стоит знать в части того как они рендерят. Рендеринг на стороне клиента может работать, но часто не без дополнительного тестирования и трудной работы. В последнее время динамический рендеринг также стал вариантом, заслуживающим внимания, если ваша архитектура в значительной степени ориентирована на клиентский JavaScript.

В случае сомнений, инструмент Mobile Friendly Test бесценен для проверки, что выбранный вами подход делает то, что бы вы хотели. Он показывает визуальный предварительный просмотр того, как какую-либо страницу видет поисковый робот Google, сериализованный HTML контент, найденный (после выполнения JavaScript), и любые ошибки, обнаруженные во время рендеринга.

Заключение...

При принятии решения о подходе к рендерингу, измеряйте и понимайте, каковы ваши "узкие места". Подумайте, может ли статический рендеринг или серверный рендеринг дать вам хотя бы 90% возможностей. Совершенно нормально обычно отправлять HTML с минимальным количеством JS, чтобы получить интерактивный опыт. Вот удобная инфографика, показывающая спектр возможностей в разрезе сервер-клиент:

Благодарности

Спасибо всем этим людям за отзывы и вдохновение:
Jeffrey Posnick, Houssein Djirdeh, Shubhie Panicker, Chris Harrelson, and Sebastian Markbage