Одно из ключевых решений, которое приходится принимать веб-разработчикам, — это выбор места реализации логики и рендеринга в их приложении. Это может быть непросто, поскольку существует множество способов создания веб-сайтов.
Наше понимание этой области основано на опыте работы с Chrome и взаимодействии с крупными сайтами за последние несколько лет. В целом, мы рекомендуем разработчикам отдавать предпочтение серверному или статическому рендерингу вместо подхода с полной регидратацией.
Чтобы лучше понимать, какие архитектуры мы выбираем при принятии решения, нам нужна единая терминология и общая структура для каждого подхода. Это позволит лучше оценить компромиссы каждого подхода к рендерингу с точки зрения производительности страницы.
Терминология
Сначала определим некоторую терминологию, которую мы будем использовать.
Рендеринг
- Рендеринг на стороне сервера (SSR)
- Рендеринг приложения на сервере для отправки клиенту HTML, а не JavaScript.
- Клиентский рендеринг (CSR)
- Отображение приложения в браузере с использованием JavaScript для изменения DOM.
- Предварительный рендеринг
- Запуск клиентского приложения во время сборки для сохранения его начального состояния в виде статического HTML.
- Гидратация
- Выполнение клиентских скриптов для добавления состояния приложения и интерактивности в HTML-код, отрендеренный на сервере. Гидратация предполагает, что DOM не изменяется.
- Регидратация
- Хотя часто под регидратацией подразумевается то же самое, что и под гидратацией, регидратация подразумевает регулярное обновление DOM до последнего состояния, в том числе после первоначальной гидратации.
Производительность
- Время до первого байта (TTFB)
- Время между нажатием на ссылку и загрузкой первого байта контента на новой странице.
- Первая контентная отрисовка (FCP)
- Время, когда запрошенный контент (текст статьи и т. д.) становится видимым.
- Взаимодействие со следующей покраской (INP)
- Репрезентативная метрика, которая оценивает, насколько быстро страница реагирует на действия пользователя.
- Общее время блокировки (TBT)
- Прокси-метрика для INP , которая вычисляет, как долго основной поток был заблокирован во время загрузки страницы.
Рендеринг на стороне сервера
Рендеринг на стороне сервера генерирует полный HTML-код страницы на сервере в ответ на навигацию. Это позволяет избежать дополнительных циклов загрузки данных и создания шаблонов на стороне клиента, поскольку рендерер обрабатывает их до того, как браузер получит ответ.
Рендеринг на стороне сервера обычно обеспечивает высокую скорость загрузки (FCP). Выполнение логики страницы и её рендеринг на сервере позволяют избежать отправки большого количества JavaScript-кода клиенту. Это помогает сократить время ожидания (TTBT) страницы, что также может привести к снижению INP, поскольку основной поток реже блокируется во время загрузки страницы. Чем реже основной поток блокируется, тем быстрее пользовательские взаимодействия выполняются.
Это логично, поскольку при серверном рендеринге вы фактически просто отправляете текст и ссылки в браузер пользователя. Этот подход может хорошо работать на различных устройствах и в различных сетях и открывает интересные возможности для оптимизации браузера, например, потоковую обработку документов.

Благодаря серверному рендерингу пользователи с меньшей вероятностью будут вынуждены ждать выполнения JavaScript, связанного с процессором, прежде чем смогут использовать ваш сайт. Даже если невозможно избежать стороннего JavaScript , использование серверного рендеринга для снижения затрат на собственный JavaScript может сэкономить вам бюджет на остальное. Однако у этого подхода есть один потенциальный недостаток: генерация страниц на сервере занимает время, что может увеличить время до достижения (TTFB) вашей страницы.
Достаточность серверного рендеринга для вашего приложения во многом зависит от того, какой тип пользовательского опыта вы разрабатываете. Ведутся давние споры о том, какое применение серверного рендеринга использовать, а какое — клиентского, но вы всегда можете выбрать серверный рендеринг для одних страниц, а какое — нет. Некоторые сайты успешно применяют гибридные методы рендеринга. Например, Netflix рендерит свои относительно статичные целевые страницы на сервере, одновременно выполняя предварительную загрузку JavaScript для страниц с интенсивным взаимодействием, что повышает вероятность быстрой загрузки этих более ресурсоемких клиентских страниц.
Благодаря множеству современных фреймворков, библиотек и архитектур вы можете визуализировать одно и то же приложение как на клиенте, так и на сервере. Эти методы можно использовать для рендеринга на стороне сервера. Однако архитектуры, в которых рендеринг выполняется как на сервере , так и на клиенте, представляют собой отдельный класс решений с совершенно разными характеристиками производительности и компромиссами. Пользователи React могут использовать серверные DOM-API или решения, созданные на их основе, такие как Next.js, для рендеринга на стороне сервера. Пользователи Vue могут использовать руководство Vue по рендерингу на стороне сервера или Nuxt . В Angular есть Universal .
Однако большинство популярных решений используют ту или иную форму гидратации, поэтому будьте в курсе подходов, которые использует ваш инструмент.
Статический рендеринг
Статический рендеринг выполняется во время сборки. Этот подход обеспечивает высокую скорость загрузки (FCP), а также более низкие время загрузки (TBT) и время загрузки (INP) при условии ограничения количества клиентского JavaScript на страницах. В отличие от серверного рендеринга, он также обеспечивает стабильно высокую скорость загрузки (TTFB), поскольку HTML-код страницы не требуется динамически генерировать на сервере. Как правило, статический рендеринг подразумевает предварительное создание отдельного HTML-файла для каждого URL-адреса. Благодаря предварительной генерации HTML-ответов можно развернуть статический рендеринг на нескольких CDN, чтобы воспользоваться преимуществами кэширования на периферии.

Решения для статического рендеринга бывают самых разных форм и размеров. Такие инструменты, как Gatsby, созданы для того, чтобы разработчики чувствовали, что их приложение отображается динамически, а не генерируется на этапе сборки. Инструменты генерации статических сайтов, такие как 11ty , Jekyll и Metalsmith, используют их статическую природу, предлагая подход, основанный на шаблонах.
Один из недостатков статического рендеринга заключается в необходимости генерировать отдельные HTML-файлы для каждого возможного URL. Это может быть сложно или даже невозможно, если нужно предсказать эти URL заранее, а также для сайтов с большим количеством уникальных страниц.
Пользователи React, возможно, знакомы с Gatsby, статическим экспортом Next.js или Navi , которые упрощают создание страниц из компонентов. Однако статический рендеринг и предварительный рендеринг ведут себя по-разному: статически отрисованные страницы интерактивны и не требуют выполнения большого количества клиентского JavaScript, тогда как предварительный рендеринг улучшает FCP одностраничного приложения, которое необходимо загружать на клиенте, чтобы сделать страницы по-настоящему интерактивными.
Если вы не уверены, является ли данное решение статическим рендерингом или предварительным рендерингом, попробуйте отключить JavaScript и загрузить страницу, которую хотите протестировать. На статически отрисовываемых страницах большинство интерактивных функций доступны и без JavaScript. На предварительно отрисовываемых страницах некоторые базовые функции, например ссылки, могут сохраняться даже при отключенном JavaScript, но большая часть страницы инертна.
Ещё один полезный тест — использовать ограничение сети в Chrome DevTools и посмотреть, сколько JavaScript-кода загружается, прежде чем страница станет интерактивной. Для пререндеринга обычно требуется больше JavaScript-кода, чтобы страница стала интерактивной, и этот JavaScript, как правило, сложнее, чем подход с прогрессивным улучшением, используемый при статическом рендеринге.
Рендеринг на стороне сервера и статический рендеринг
Рендеринг на стороне сервера — не лучшее решение для любых задач, поскольку его динамическая природа может привести к значительным вычислительным затратам. Многие решения для рендеринга на стороне сервера не выполняют раннюю очистку, задерживают TTFB или удваивают объём отправляемых данных (например, встроенные состояния, используемые JavaScript на клиенте). В React renderToString() может быть медленным, поскольку он синхронный и однопоточный. Новые серверные DOM-API React поддерживают потоковую передачу, что позволяет быстрее получать начальную часть HTML-ответа в браузер, пока остальная часть ещё формируется на сервере.
Для «правильного» рендеринга на стороне сервера может потребоваться поиск или создание решения для кэширования компонентов , управления потреблением памяти, использования методов мемоизации и решения других задач. Часто одно и то же приложение обрабатывается или перестраивается дважды: один раз на клиенте, а второй раз на сервере. Более раннее отображение контента на стороне сервера не обязательно означает сокращение объёма работы. Если после получения клиентом HTML-ответа, сгенерированного сервером, на стороне клиента выполняется много работы, это может привести к увеличению TBT и INP вашего сайта.
Рендеринг на стороне сервера создаёт HTML-код по запросу для каждого URL, но он может быть медленнее, чем просто статическая отрисовка контента. При наличии дополнительных усилий серверный рендеринг в сочетании с кэшированием HTML может значительно сократить время рендеринга. Преимущество серверного рендеринга заключается в возможности извлекать больше «живых» данных и отвечать на более полный набор запросов, чем при статической отрисовке. Страницы, требующие персонализации, — яркий пример запроса, который плохо работает при статической отрисовке.
Рендеринг на стороне сервера также может представлять интересные решения при создании PWA . Что лучше: использовать кэширование всей страницы сервис-воркера или рендерить отдельные фрагменты контента на сервере?
Рендеринг на стороне клиента
Клиентский рендеринг подразумевает рендеринг страниц непосредственно в браузере с помощью JavaScript. Вся логика, выборка данных, шаблонизация и маршрутизация обрабатываются на клиенте, а не на сервере. В результате на устройство пользователя передается больше данных с сервера, что имеет свои недостатки.
Клиентский рендеринг может быть сложно реализовать и поддерживать на мобильных устройствах. Приложив немного усилий, чтобы уложиться в ограниченный бюджет JavaScript и обеспечить отдачу за минимальное количество циклов загрузки , вы сможете добиться того, чтобы клиентский рендеринг практически повторял производительность чисто серверного рендеринга. Вы можете ускорить работу парсера, предоставляя критически важные скрипты и данные с помощью <link rel=preload> Мы также рекомендуем рассмотреть возможность использования шаблонов, таких как PRPL, чтобы обеспечить мгновенную начальную и последующие навигацию.

Основным недостатком клиентского рендеринга является то, что объём необходимого JavaScript-кода, как правило, растёт по мере роста приложения, что может повлиять на производительность страницы. Это становится особенно сложно с добавлением новых JavaScript-библиотек, полифиллов и стороннего кода, которые конкурируют за вычислительную мощность и часто должны быть обработаны перед отображением содержимого страницы.
В приложениях, использующих клиентский рендеринг и использующих большие пакеты JavaScript, следует рассмотреть возможность агрессивного разделения кода для снижения TBT и INP при загрузке страницы, а также отложенной загрузки JavaScript для предоставления только того, что нужно пользователю, и тогда, когда это необходимо. Для приложений с низкой или нулевой интерактивностью серверный рендеринг может представлять собой более масштабируемое решение этих проблем.
Для разработчиков одностраничных приложений определение основных элементов пользовательского интерфейса, общих для большинства страниц, позволяет применять метод кэширования оболочки приложения . В сочетании с сервис-воркерами это может значительно повысить воспринимаемую производительность при повторных посещениях, поскольку страница может очень быстро загружать HTML-код оболочки приложения и зависимости из CacheStorage .
Регидратация объединяет рендеринг на стороне сервера и клиента.
Гидратация — это подход, позволяющий смягчить компромиссы между клиентской и серверной обработкой данных, выполняя обе. Навигационные запросы, такие как полная загрузка или перезагрузка страницы, обрабатываются сервером, который преобразует приложение в HTML. Затем JavaScript и данные, используемые для рендеринга, встраиваются в результирующий документ. При грамотном подходе достигается быстрый рендеринг FCP на стороне сервера, а затем он «подхватывается» повторным рендерингом на стороне клиента.
Это эффективное решение, но оно может иметь существенные недостатки в производительности.
Основной недостаток серверного рендеринга с регидратацией заключается в том, что он может существенно снизить время загрузки и время загрузки, даже если улучшает FCP. Страницы, отрисованные на сервере, могут выглядеть загруженными и интерактивными, но не смогут реагировать на ввод данных, пока не будут выполнены клиентские скрипты для компонентов и не будут подключены обработчики событий. На мобильных устройствах это может занять несколько минут, что сбивает с толку и раздражает пользователя.
Проблема регидратации: одно приложение по цене двух
Чтобы клиентский JavaScript-код точно выполнял обработку с того места, на котором остановился сервер, без повторного запроса всех данных, которые сервер использовал для рендеринга HTML, большинство серверных решений для рендеринга сериализуют ответ от зависимостей данных пользовательского интерфейса в виде тегов скрипта в документе. Поскольку это дублирует большой объём HTML-кода, повторная гидратация может привести к более серьёзным проблемам, чем просто задержка интерактивности.

Сервер возвращает описание пользовательского интерфейса приложения в ответ на навигационный запрос, а также исходные данные, использованные для его создания, и полную копию реализации пользовательского интерфейса, которая затем загружается на клиенте. Пользовательский интерфейс становится интерактивным только после завершения загрузки и выполнения bundle.js .
Показатели производительности, собранные с реальных веб-сайтов, использующих серверный рендеринг и регидратацию, показывают, что это редко является лучшим вариантом. Главная причина — его влияние на пользовательский опыт, когда страница выглядит готовой, но ни одна из её интерактивных функций не работает.

Есть надежда на серверный рендеринг с регидратацией. В краткосрочной перспективе только серверный рендеринг для контента с высокой степенью кэширования может снизить время до полной загрузки, давая результаты, аналогичные результатам пререндеринга. Постепенная , прогрессивная или частичная регидратация может стать ключом к повышению жизнеспособности этой технологии в будущем.
Потоковая обработка на стороне сервера и постепенная регидратация
За последние несколько лет рендеринг на стороне сервера претерпел ряд изменений.
Потоковая серверная обработка позволяет отправлять HTML-код фрагментами, которые браузер может постепенно отображать по мере поступления. Это позволяет быстрее доставлять разметку пользователям, ускоряя FCP. В React асинхронность потоков в renderToPipeableStream() по сравнению с синхронной renderToString() означает, что обратное давление обрабатывается эффективно.
Также стоит рассмотреть возможность прогрессивной регидратации ( в React она реализована ). При таком подходе отдельные фрагменты серверного приложения «загружаются» постепенно, вместо того, чтобы в настоящее время инициализировать всё приложение сразу. Это может помочь сократить объём JavaScript-кода, необходимого для обеспечения интерактивности страниц, поскольку позволяет отложить обновление низкоприоритетных частей страницы на стороне клиента, чтобы предотвратить блокировку основного потока, позволяя пользователю взаимодействовать с ними быстрее после того, как он их инициирует.
Прогрессивная регидратация также может помочь вам избежать одной из самых распространенных ловушек регидратации рендеринга на стороне сервера: дерево DOM, визуализированное сервером, уничтожается, а затем немедленно восстанавливается, чаще всего из-за того, что первоначальный синхронный рендеринг на стороне клиента требовал данных, которые были не совсем готовы, часто Promise , который еще не был разрешен.
Частичная регидратация
Частичная регидратация оказалась сложной в реализации. Этот подход представляет собой расширение прогрессивной регидратации, которая анализирует отдельные фрагменты страницы (компоненты, представления или деревья) и выявляет фрагменты с низкой интерактивностью или без неё. Для каждого из этих преимущественно статических фрагментов соответствующий код JavaScript затем преобразуется в инертные ссылки и декоративные элементы, сводя их влияние на клиентскую сторону практически к нулю.
Подход с частичной регидратацией имеет свои недостатки и компромиссы. Он создаёт ряд интересных проблем для кэширования, а навигация на стороне клиента означает, что мы не можем предполагать, что HTML-код, отрендеренный сервером для инертных частей приложения, будет доступен без полной загрузки страницы.
Трисоморфный рендеринг
Если вам подходит сервис-воркер , рассмотрите трисоморфный рендеринг. Этот метод позволяет использовать потоковый рендеринг на стороне сервера для первоначальной навигации или навигации без JavaScript, а затем поручить сервис-воркеру отрисовку HTML для навигации после его установки. Это позволяет поддерживать кешированные компоненты и шаблоны в актуальном состоянии и использовать навигацию в стиле SPA для рендеринга новых представлений в том же сеансе. Этот подход лучше всего работает, когда сервер, клиентская страница и сервис-воркер могут использовать один и тот же код шаблонизации и маршрутизации.

SEO-соображения
При выборе стратегии веб-рендеринга команды часто учитывают влияние SEO. Рендеринг на стороне сервера — популярный вариант для создания «полного» пользовательского опыта, понятного поисковым роботам. Роботы понимают JavaScript , но их рендеринг часто имеет ограничения . Клиентский рендеринг может работать, но часто требует дополнительного тестирования и дополнительных затрат. В последнее время динамический рендеринг также стал вариантом, который стоит рассмотреть, если ваша архитектура сильно зависит от клиентского JavaScript.
Если вы сомневаетесь, инструмент проверки совместимости с мобильными устройствами — отличный способ проверить, соответствует ли выбранный вами подход вашим ожиданиям. Он показывает визуальное представление того, как страница отображается для поискового робота Google, сериализованный HTML-контент, который он обнаруживает после выполнения JavaScript, и любые ошибки, возникшие при рендеринге.

Заключение
Выбирая подход к рендерингу, оцените и поймите, в чём заключаются ваши узкие места. Подумайте, какой рендеринг — статический или серверный — поможет вам в большей степени решить эту проблему. Для достижения интерактивного взаимодействия вполне допустимо использовать в основном HTML с минимальным количеством JavaScript. Вот удобная инфографика, демонстрирующая спектр сервер-клиент:

Кредиты {:#credits}
Спасибо всем за отзывы и вдохновение:
Джеффри Посник, Хуссейн Джирдех, Шубхи Паникер, Крис Харрельсон и Себастьян Маркбоге.