Написать пост

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

Аватар Никита Прияцелюк

Обложка поста Серверный или клиентский рендеринг на вебе: что лучше использовать у себя в проекте и почему

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

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

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

Отображение:

  • SSR (Server-Side Rendering, серверный рендеринг) — рендеринг на сервере клиентской части или универсального приложения в HTML;
  • CSR (Client-Side Rendering, рендеринг на клиенте) — рендеринг приложения на стороне клиента (в браузере), обычно с помощью DOM;
  • Регидратация — «загрузка» JavaScript компонентов на клиенте таким образом, чтобы они повторно использовали DOM-дерево и данные HTML, сформированные на стороне сервера;
  • Пререндеринг — запуск клиентского приложения во время сборки для сохранения его начального состояния в виде статического HTML.

Производительность:

  • TTFB (Time to First Byte, время до первого байта) — время между кликом по ссылке и поступлением первого фрагмента содержимого;
  • FP (First Paint, первая отрисовка) — первый случай, когда пользователю становится виден любой пиксель;
  • FCP (First Contentful Paint, первая содержательная отрисовка) — время первого отображения запрашиваемого содержимого (например, статьи);
  • TTI (Time To Interactive, время до интерактивности) — момент времени, в который страница становится интерактивной (закрепляются все события и т. д.).

Серверный рендеринг

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

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

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

Ответ на вопрос «достаточно ли серверного рендеринга для моего приложения?» зависит от того, что вы создаёте. Сообщество давно дискутирует на тему правильного применения серверного рендеринга против клиентского, но важно помнить, что для одних страниц использовать серверный рендеринг можно, а для других — нет. Некоторые сайты успешно используют смешанный рендеринг. Netflix генерирует на сервере относительно статические лендинги и в то же время предварительно загружает JavaScript для страниц с высоким уровнем интерактивности, давая возможность быстрее загрузиться страницам, которые больше используют клиентский рендеринг.

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

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

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

Для статического рендеринга существуют самые разные решения. Инструменты вроде Gatsby разработаны так, чтобы создавать впечатление динамического рендеринга. Другие, вроде Jekyl и Metalsmith, принимают свою статическую природу и предлагают подход, основанный в большей степени на шаблонах.

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

Пользователи React могут быть знакомы с Gatsby, статическим экспортом Next.js или Navi — все они делают использование компонентов удобнее. Однако важно понимать разницу между статическим рендерингом и пререндерингом: статически отрендеренные страницы не нуждаются в выполнении большого количества клиентского JS для интерактивности, в то время как пререндеринг улучшает первую (содержательную) отрисовку одностраничного приложения, которое должно быть загружено на клиент, чтобы страницы были действительно интерактивными.

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

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

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

Серверное отображение не панацея — его динамическая природа может сопровождаться множественными вычислительными затратами. Многие решения с серверным отображением не используют технологию early flush и могут оттянуть время до первого байта или удвоить количество отправляемых данных (например, встроенное состояние, используемое JS на клиенте). В React метод renderToString() может быть медленным из-за синхронности и однопоточности. Для эффективной реализации серверного рендеринга может потребоваться найти решение для кеширования компонентов, разобраться с управлением потребления памяти, применить мемоизацию и не только. По сути вы заново обрабатываете/собираете одно приложение несколько раз — на сервере и на клиенте. Тот факт, что серверный рендеринг может показать что-то быстрее, вовсе не означает, что нужно проделать меньше вычислительной работы.

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

Клиентский рендеринг

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

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

Основной недостаток клиентского рендеринга заключается в том, что количество необходимого JavaScript обычно увеличивается вместе с ростом приложения. Ситуация ухудшается с подключением новых JavaScript-библиотек, полифиллов и прочего стороннего кода, который соревнуется между собой за вычислительные мощности и часто требует обработки, прежде чем содержимое страницы можно будет отобразить. Решениям с клиентским рендерингом, которые полагаются на большие JavaScript-бандлы, стоит рассмотреть сильное разделение кода и ленивую загрузку JavaScript — «загружайте только то, что вам нужно и только когда это нужно». Для решений с минимумом интерактивности или её отсутствием серверный рендеринг может предоставить более масштабируемое решение этих проблем.

Если вы создаёте одностраничное приложение, то, определив основные части пользовательского интерфейса, которые используются на большинстве страниц, вы сможете использовать кеширование оболочки приложения. В сочетании с Service Worker’ами это даст сильный прирост ощущаемой производительности при повторных посещениях.

Совмещение серверного и клиентского рендеринга с помощью регидратации

Универсальный рендеринг (или просто «SSR») пытается устранить недостатки серверного и клиентского рендеринга, используя оба подхода. Навигационные запросы вроде полной загрузки или перезагрузки страницы обрабатываются сервером, который рендерит приложение в HTML, затем JavaScript и данные, используемые для рендеринга, встраиваются в итоговый документ. При правильной реализации время первой содержательной отрисовки будет как при серверном рендеринге, а повторный рендеринг будет производиться на клиенте с помощью техники, называемой (ре)гидратацией. Это новое решение, тем не менее не лишённое определённых проблем с производительностью.

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

Возможно, вы и сами сталкивались с таким — страница уже какое-то время выглядит загруженной, но нажатия на элементы не дают эффекта. Это сильно удручает: «Почему ничего не происходит? Почему я не могу скроллить?».

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

Из-за JavaScript проблемы регидратации могут быть похуже, чем отложенная интерактивность. Для того чтобы клиентский JavaScript мог, не прибегая к новому запросу всех данных, использованных сервером для рендеринга HTML, продолжить работу с того места, где прекратил работу сервер, текущие решения на основе универсального рендеринга обычно сериализуют данные для интерфейса в документ в виде тегов <script>. Итоговый HTML содержит большое количество повторяющейся информации:

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

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

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

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

За последние несколько лет в области серверного рендеринга появился ряд новых разработок.

Потоковый серверный рендеринг позволяет посылать HTML фрагментами, которые браузер может постепенно рендерить по мере получения. Это может дать быстрые первые отрисовки, так как пользователи получают разметку быстрее. В React использование асинхронного renderToNodeStream() вместо синхронного renderToString() поможет улучшить производительность.

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

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

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

У частичной регидратации есть свои недостатки. Реализация кеширования может оказаться не из простых, а клиентская навигация подразумевает, что нельзя быть уверенным, что HTML, сгенерированный на сервере для «инертных» частей приложения, будет доступен без полной загрузки страницы.

Трисоморфный рендеринг

Если вы используете Service Worker’ы, то вас может заинтересовать «трисоморфный» рендеринг. Это метод, при котором используется потоковый серверный рендеринг для начальных/не-JS навигаций, а затем Service Worker после установки рендерит остальной HTML для навигации. Это позволяет поддерживать кешированные компоненты и шаблоны в актуальном состоянии и даёт возможность использовать SPA-навигации для рендеринга новых представлений в том же сеансе. Данный подход работает лучше всего, когда вы можете использовать одни и те же шаблоны и код маршрутизации для сервера, клиентской страницы и Service Worker’а.

Немного SEO

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

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

Итог

Прежде чем выбрать подход для рендеринга, подумайте об узких местах вашего проекта. А с выбором поможет эта инфографика:

Веб-разработка
Лучшая практика
71852