что такое рендеринг на сервере
Рендеринг в веб
о переводе
Наше понимание в этой области основано на нашей работе с Chrome, и контактировании с большими сайтами в течение последних нескольких лет. В общем, мы хотим вдохновить разработчиков рассмотреть использование серверного рендеринга или статического рендеринга с полноценной регидратацией.
Чтобы лучше понимать архитектуры, из которых мы выбираем, когда принимаем решение, нам необходимо иметь четкое понимание каждого подхода и последовательную терминологию, которую мы будем использовать, когда говорим о них. Различия между этими подходами помогают проиллюстрировать компромиссы при рендеринге в вебе через призму производительности.
Терминология
Рендеринг
Rehydration (регидратация): «загрузка» JavaScript отображениий на клиенте таким образом, чтобы они повторно использовали отрендеренное на сервере DOM-дерево и данные HTML-а
Prerendering (пре-рендеринг): выполнение клиентского приложения во время сборки для захвата его начального состояния в виде статического HTML.
Performance
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 (Статический рендеринг)
Решения для статического рендеринга бывают разных форм и размеров. Такие инструменты как Gatsby разработаны для того, чтобы разработчики чувствовали, что их приложение отрисовывается динамически, а не генерируется на этапе сборки. Другие, такие как Jekyll и Metalsmith, принимают их статическую природу, предоставляя подход более заточенный на шаблоны.
Одним из недостатков статического рендеринга является то, что отдельные HTML-файлы должны быть сгенерированы для каждого возможного URL. Это может быть сложно или даже невозможно, когда вы не можете предсказать, какими будут эти URL заранее, или если на сайте большое количество уникальных страниц.
Если вы не уверены, является ли решение статическим рендерингом или пре-рендерингом, попробуйте такой тест: отключите JavaScript и загрузите созданные веб-страницы. У статически отрендеренных страниц бОльшая часть функционала все равно будет существовать и без включённого JavaScript. У пре-рендеренных страниц все еще может быть некоторая базовая функциональность, такая как ссылки, но бОльшая часть страницы будет неживой.
Серверный рендеринг против статического
Серверный рендеринг генерирует HTML по требованию для каждого URL, но это может быть медленнее, чем просто обслуживание статически отрендереного контента. Если вы готовы сделать дополнительные усилия, то серверный рендеринг + [HTML кеширование] (https://freecontent.manning.com/caching-in-react/) может значительно сократить время серверного рендеринга. Положительной стороной серверного рендеринга является возможность получать более «живые» данные и отвечать на более полный набор запросов, чем это возможно при статическом рендеринге. Страницы, требующие персонализации, являются хорошим примером типа запроса, который плохо работает со статическим рендерингом.
Серверный рендеринг также может представлять интересные решения при построении PWA. Лучше ли использовать full-page service worker кеширование, или просто рендерить на сервере отдельные фрагменты контента?
Client-Side Rendering (CSR)
Рендеринг на стороне клиента (CSR) означает рендеринг страниц непосредственно в браузере с использованием 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), давая результаты, схожие с пре-рендерингом. Регидратация инкрементальная, прогрессивная или частичная, может быть ключом к тому, чтобы сделать эту технику более жизнеспособной в будущем.
Потоковый серверный рендеринг и прогрессивная регидратация
Серверный рендеринг за последние несколько лет претерпел ряд изменений.
= Частичная регидратация
Частичная регидратация оказалась трудной для осуществления. Этот подход является продолжением идеи прогрессивной регидратации, когда отдельные части (компоненты / виджеты / деревья), подлежащие прогрессивной регидратации, анализируются, а те, которые обладают низкой интерактивностью или не обладают реактивностью помечаются. Для каждой из этих наиболее статических частей соответствующий код 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 Markbåge
Рендеринг на клиенте, на сервере и генерация статических сайтов
Приветствую всех профессионалов и любителей сайтостроения! Предлагаю вашему вниманию перевод статьи «Client-Side Rendering vs Server-Side Rendering vs Static-Site Generation» от Malcolm Laing.
Фронтендеры часто используют эти термины для описания своих приложений. Однако людей, хуже знакомых с веб-технологиямм, эти понятия часто вводят в заблуждение. Если вам сложно понять различия между рендерингом на стороне клиенте, рендерингом на стороне сервера и генерацией статических сайтов — эта статья для вас!
Рендеринг на стороне клиента
Рендеринг на клиенте стал популярен с расцветом технологии Single Page Application (Spa). Этот подход применяется во многих JavaScript фреймворках, например AngularJS, ReactJS, Backbone.JS и др. В приложениях с генерацией на клиенте сервер отправляет JS файлы и статичный HTML на сторону клиента. Затем клиент выполняет вызовы API в количестве, достаточном для того, чтобы получить исходные данные, и уже после этого приложение рендерится.
Как видно на примере выше, отданный сервером статичный HTML — это пустая страница. Если открыть этот HTML с отключенным JS, результатом будет пустой экран с ворнингом, записанным в теге noscript. Когда клиент получает HTML и подгружает JS, в div с ID root отрендерится React-приложение.
Преимущества рендеринга на клиенте
Недостатки рендеринга на клиенте
Рендеринг на стороне сервера
Приложения с серверным рендерингом отдают готовые к рендеру HTML страницы. Такие приложения заранее выполняют все необходимые вызовы API и передают все необходимые данные в изначальном запросе. Это значит, что браузер сразу же получает все, что нужно для рендеринга приложения, что сокращает время до первого взаимодействия с пользователем.
Серверный рендеринг — это классический метод создания сайтов. Традиционным недостатком этого метода была необходимость выполнять серию запросов к серверу по мере навигации пользователя по сайту. Однако с помощью таких инструментов, как NextJS, мы можем создавать приложения, сочетающие в себе лучшие стороны генерации на сервере и на клиенте. Способ заключается в том, чтобы осуществить первую загрузку после рендеринга на стороне сервера, а маршрутизацию на стороне клиента.
Преимущества рендеринга на стороне сервера
Недостатки рендеринга на стороне сервера
Генерация статических сайтов
Генераторы статических сайтов работают, генерируя все HTML-файлы для сайта во время сборки. Сервер заранее выполняет вызовы API и генерирует статические HTML-файлы для каждой страницы вашего сайта. Это значит, что когда клиент запрашивает одну из веб-страниц, серверу не нужно обращаться к API или рендерить HTML, ему нужно только вернуть предварительно подготовленную HTML-страницу.
Допустим, вы создаете блог и написали десять постов. Во время сборки вашего статического сайта он сгенерирует HTML-файл для каждого сообщения в блоге. Когда вы напишете еще один пост, вам нужно пересобрать приложение и задеплоить обновленную статику.
Gatsby и NextJS — два наиболее известных генератора статических сайтов, основанных на React. Hugo — еще один пример чрезвычайно популярного генератора статических сайтов.
Преимущества создания статических сайтов
Недостатки создания статических сайтов
Лучшее из обоих миров — NextJS
На мой взгляд, NextJS предлагает комбинирует лучшее из обоих подходов, позволяя нам создавать гибридные приложения, которые используют как рендеринг на стороне сервера, так и создание статических сайтов. NextJS предлагает то, что в рамках фреймворка называется автоматической статической оптимизацией для тех страниц, которые он определяет как статические. Это позволяет создавать гибридные приложения, содержащие как страницы, рендерящиеся на сервере, так и сгенерированную статику.
Серверный рендеринг в бессерверной среде
Автор материала, перевод которого мы публикуем, является одним из основателей проекта Webiny — бессерверной CMS, основанной на React, GraphQL и Node.js. Он говорит, что поддержка многоарендной бессерверной облачной платформы — это дело, которому свойственны особенные задачи. Написано уже много статей, в которых идёт речь о стандартных техниках оптимизации веб-проектов. Среди них — серверный рендеринг, использование технологий разработки прогрессивных веб-приложений, разные способы улучшения сборок приложений и многое другое. Эта статья, с одной стороны, похожа на другие, а с другой — от них отличается. Дело в том, что она посвящена оптимизации проектов, работающих в бессерверной среде.
Подготовка
Для того чтобы проводить измерения, которые помогут идентифицировать проблемы проекта, мы будем использовать webpagetest.org. С помощью этого ресурса мы будем выполнять запросы и собирать сведения о времени выполнения различных операций. Это позволит нам лучше понять то, что видят и ощущают пользователи, работая с проектом.
Нас особенно интересует показатель «First view», то есть — то, сколько времени занимает загрузка сайта у пользователя, который посещает его впервые. Это — очень важный показатель. Дело в том, что кэш браузера способен скрывать многие узкие места веб-проектов.
Показатели, отражающие особенности загрузки сайта — идентификация проблем
Взгляните на следующую диаграмму.
Анализ старых и новых показателей веб-проекта
Здесь самым важным показателем можно признать «Time to Start Render» — время до начала рендеринга. Если присмотреться к этому показателю, то можно увидеть, что только для того, чтобы начать рендеринг страницы, в старой версии проекта требовалось почти 2 секунды. Причина этого кроется в самой сущности одностраничных приложений (Single Page Application, SPA). Для того чтобы вывести страницу подобного приложения на экран, сначала надо загрузить объёмный JS-бандл (этот этап загрузки страницы отмечен на следующем рисунке как 1). Потом этот бандл нужно обработать в главном потоке (2). И уже только после этого в окне браузера может что-то появиться.
(1) Загрузка JS-бандла. (2) Ожидание обработки бандла в главном потоке
Однако это — лишь часть общей картины. После того, как главный поток обработает JS-бандл, он выполняет несколько запросов к API Gateway. На этой стадии обработки страницы пользователь видит вращающийся индикатор загрузки. Зрелище это не самое приятное. При этом пользователь пока ещё не видел никакого содержимого страницы. Вот раскадровка процесса загрузки страницы.
Всё это говорит о том, что пользователь, посетивший такой сайт, испытывает не особенно приятные ощущения от работы с ним. А именно, он вынужден в течение 2 секунд смотреть на пустую страницу, а потом ещё секунду — на индикатор загрузки. Эта секунда добавляется ко времени подготовки страницы из-за того, что после загрузки и обработки JS-бандла выполняются запросы к API. Эти запросы необходимы для того, чтобы загрузить данные и, в результате, вывести на экран готовую страницу.
Если бы проект хостился на обычном VPS, то временные затраты на выполнение этих запросов к API были бы, в основном, предсказуемыми. Однако на проекты, работающие в бессерверной среде, влияет печально известная проблема «холодного старта». В случае с облачной платформой Webiny дело обстоит ещё хуже. Функции AWS Lambda являются частью VPC (Virtual Private Cloud, виртуальное частное облако). Это означает, что для каждого нового экземпляра такой функции нужно инициализировать ENI (Elastic Network Interface, эластичный сетевой интерфейс). Это значительно увеличивает время холодного старта функций.
Вот некоторые временные показатели, касающиеся загрузки функций AWS Lambda внутри VPC и за пределами VPC.
Анализ загрузки функций AWS Lambda внутри VPC и за пределами VPC (изображение взято отсюда)
Из этого можно сделать вывод о том, что в том случае, когда функция запускается внутри VPC, это даёт 10-кратное увеличение времени холодного старта.
Кроме того, тут нужно учитывать и ещё один фактор — задержки сетевой передачи данных. Их длительность уже включена в то время, которое необходимо на выполнение запросов к API. Запросы инициирует браузер. Поэтому получается, что ко времени реакции API на эти запросы добавляется время, необходимое на то, чтобы запрос дошёл от браузера к API, и время, которое нужно для того, чтобы ответ добрался бы от API к браузеру. Эти задержки возникают при выполнении каждого запроса.
Задачи оптимизации
Мы, основываясь на вышеприведённом анализе, сформулировали несколько задач, которые нам нужно было решить для оптимизации проекта. Вот они:
Подходы к решению задач
Вот несколько подходов к решению задач, которые мы рассматривали:
Первой идеей, которая показалась нам интересной, было создание HTML-снимка отрендеренной страницы и передача этого снимка пользователям.
Неудачная попытка
Webiny Cloud — это бессерверная инфраструктура, основанная на AWS Lambda, которая поддерживает сайты Webiny. Наша система умеет выявлять ботов. Когда оказывается, что запрос выполнен ботом, этот запрос перенаправляется экземпляру Puppeteer, который рендерит страницу, используя Chrome без пользовательского интерфейса. Боту передаётся уже готовый HTML-код страницы. Сделано это, в основном, по SEO-соображениям, из-за того, что многие боты не умеют выполнять JavaScript. Мы решили воспользоваться таким же подходом и для подготовки страниц, предназначенных для обычных пользователей.
Подобный подход хорошо показывает себя в окружениях, в которых нет поддержки JavaScript. Однако если попытаться отдавать предварительно отрендеренные страницы клиенту, браузер которого поддерживает JS, то страница выводится, но потом, после загрузки JS-файлов, React-компоненты просто не знают о том, куда им монтироваться. Это приводит к появлению целой кучи сообщений об ошибках в консоли. В результате нас подобное решение не устроило.
Знакомство с SSR
Сильная сторона серверного рендеринга (SSR, Server Side Rendering) заключается в том, что все запросы к API выполняются в пределах локальной сети. Так как они обрабатываются некоей системой или функцией, выполняющейся внутри VPC, для них нехарактерны задержки, возникающие при выполнении запросов из браузера к бэкенду ресурса. Хотя и при таком сценарии сохраняется проблема «холодного старта».
Дополнительным преимуществом использования SSR является то, что мы передаём клиенту такую HTML-версию страницы, при работе с которой после загрузки JS-файлов у компонентов React не возникает проблем с монтированием.
И, наконец, нам не нужен JS-бандл очень большого размера. Мы, кроме того, можем, для вывода страницы, обойтись без обращений к API. Бандл может быть загружен асинхронно и это не будет блокировать главный поток.
В целом можно сказать, что серверный рендеринг, вроде бы, должен был решить большинство наших проблем.
Вот как выглядит анализ сайта после применения серверного рендеринга.
Показатели сайта после применения серверного рендеринга
Теперь запросы к API не выполняются, а страницу можно увидеть до того, как загрузится большой JS-бандл. Но если присмотреться к первому запросу — можно увидеть, что на то, чтобы получить документ с сервера, уходит почти 2 секунды. Поговорим об этом.
Проблема с TTFB
Здесь мы обсудим показатель TTFB (Time To First Byte, время до первого байта). Вот подробные сведения о первом запросе.
Сведения о первом запросе
Для обработки этого первого запроса нам нужно сделать следующее: запустить Node.js-сервер, выполнить серверный рендеринг, производя запросы к API и выполняя JS-код, после чего — вернуть клиенту итоговый результат. Проблема тут заключается в том, что всё это, в среднем, занимает 1-2 секунды.
Нашему серверу, который выполняет серверный рендеринг, нужно выполнить всю эту работу, и только после этого он сможет передать клиенту первый байт ответа. Это и приводит к тому, что браузеру приходится очень долго ждать начала поступления ответа на запрос. В результате оказывается, что теперь для вывода страницы нужно произвести почти тот же объём работы, что и раньше. Разница лишь в том, что эта работа проводится не на стороне клиента, а на сервере, в процессе серверного рендеринга.
Тут у вас может возникнуть вопрос по поводу слова «сервер». Мы ведь всё это время говорим о бессерверной системе. Откуда тут взялся этот «сервер»? Мы, безусловно, пытались выполнять серверный рендеринг в функциях AWS Lambda. Но оказалось, что это — очень ресурсозатратный процесс (в частности, нужно было очень сильно увеличить объём памяти для того чтобы получить больше процессорных ресурсов). Кроме того, сюда ещё добавляется и проблема «холодного старта», о которой мы уже говорили. В результате тогда идеальным решением было использование Node.js-сервера, который загружал бы материалы сайта и занимался бы их серверным рендерингом.
Вернёмся к последствиям применения серверного рендеринга. Взгляните на следующую раскадровку. Несложно заметить то, что она не особенно отличается от той, которая была получена при исследовании проекта, который рендерился на клиенте.
Загрузка страницы при использовании серверного рендеринга
Пользователь вынужден смотреть на пустую страницу в течение 2.5 секунд. Это печально.
Хотя, глядя на эти результаты, можно подумать, что мы совершенно ничего не добились, это, на самом деле, не так. У нас был HTML-снимок страницы, содержащий всё необходимое. Этот снимок был готов к работе с React. При этом в ходе обработки страницы на клиенте не нужно было выполнять никаких запросов к API. Все необходимые данные уже были внедрены в HTML.
Единственной проблемой было то, что создание этого HTML-снимка занимало слишком много времени. В этот момент мы могли либо вложить больше времени в оптимизацию серверного рендеринга, либо просто кэшировать его результаты и отдавать клиентам снимок страницы из чего-то вроде кэша Redis. Мы поступили именно так.
Кэширование результатов серверного рендеринга
После того, как пользователь посещает сайт Webiny, мы, в первую очередь, проверяем централизованный кэш Redis на предмет того, имеется ли там HTML-снимок страницы. Если это так — мы отдаём пользователю страницу из кэша. В среднем это снизило показатель TTFB до уровня 200-400 мс. Именно после внедрения кэша мы начали замечать значительные улучшения в производительности проекта.
Загрузка страницы при использовании серверного рендеринга и кэша
Даже пользователь, который посещает сайт впервые, видит содержимое страницы меньше чем через секунду.
Посмотрим на то, как теперь выглядит waterfall-диаграмма.
Показатели сайта после применения серверного рендеринга и кэширования
Красная линия указывает на временную отметку, равную 800 мс. Именно здесь содержимое страницы оказывается полностью загруженным. Кроме того, тут можно видеть, что JS-бандлы оказываются загруженными примерно на отметке в 1.3 с. Но это не влияет на то время, которое нужно пользователю для того, чтобы увидеть страницу. При этом для вывода страницы не нужно выполнять запросы к API и нагружать главный поток.
Обратит внимание на то, что временные показатели, касающиеся загрузки JS-бандла, выполнения запросов к API, выполнения операций в главном потоке, всё ещё играют важную роль в подготовке страницы к работе. Эти затраты времени и ресурсов требуются для того, чтобы страница стала бы «интерактивной». Но это не играет никакой роли, во-первых, для ботов поисковых систем, а во-вторых — для формирования у пользователей ощущения «быстрой загрузки страницы».
Предположим, что некая страница является «динамической». Она, например, выводит в заголовке ссылку для доступа к учётной записи пользователя в том случае, если пользователь, который просматривает страницу, вошёл в систему. После выполнения серверного рендеринга в браузер поступит страница общего назначения. То есть — такая, которая выводится пользователям, не вошедшим в систему. Заголовок этой страницы изменится, отразив факт входа пользователя в систему, только после того, как будет загружен JS-бандл и будут выполнены обращения к API. Тут мы имеем дело с показателем TTI (Time To Interactive, время до первой интерактивности).
Через несколько недель мы обнаружили, что наш прокси-сервер не закрывает соединение с клиентом там, где это нужно, в том случае, если выполнение серверного рендеринга запускалось в виде фонового процесса. Исправление буквально одной строчки кода привело к тому, что показатель TTFB удалось снизить до уровня 50-90 мс. В результате сайт теперь начал выводиться в браузере примерно через 600 мс.
Однако перед нами встала ещё одна проблема…
Проблема инвалидации кэша
«В компьютерной науке есть только две сложные вещи: инвалидация кэша и именование сущностей».
Фил Карлтон
Инвалидация кэша — это, и правда, очень сложная задача. Как её решить? Во-первых, можно часто обновлять кэш, задавая очень короткое время хранения кэшированных объектов (TTL, Time To Live, время жизни). Это иногда будет приводить к тому, что страницы будут загружаться медленнее, чем обычно. Во-вторых, можно создать механизм инвалидации кэша, основанный на неких событиях.
В нашем случае данная проблема была решена с использованием очень маленького показателя TTL, равного 30 секундам. Но мы, кроме того, реализовали возможность предоставления клиентам устаревших данных из кэша. В то время, когда клиенты получают подобные данные, обновление кэша ведётся в фоновом режиме. Благодаря этому мы избавились от проблем, вроде задержек и «холодного старта», которые свойственны для функций AWS Lambda.
Вот как это работает. Пользователь посещает сайт Webiny. Мы проверяем HTML-кэш. Если там имеется снимок страницы — мы отдаём его пользователю. Возраст снимка может быть равен даже нескольким дням. Мы же, передавая пользователю этот старый снимок за несколько сотен миллисекунд, параллельно запускаем задачу по созданию нового снимка и по обновлению кэша. На выполнение этой задачи обычно уходит несколько секунд, так как мы создали механизм, благодаря которому у нас всегда есть некоторое количество заранее запущенных, готовых к работе функций AWS Lambda. Поэтому нам не приходится, во время создания новых снимков, тратить время на холодный запуск функций.
В результате мы всегда возвращаем клиентам страницы из кэша, а когда возраст кэшированных данных достигает 30 секунд — содержимое кэша обновляется.
Кэширование — это, определённо, та область, в которой мы ещё можем кое-что улучшить. Например, мы рассматриваем возможность автоматического обновления кэша в том случае, когда пользователь публикует страницу. Однако такой механизм обновления кэша тоже не идеален.
Например, предположим, что на домашней странице ресурса выводится три самых свежих публикации из блога. Если кэш обновляется при публикации новой страницы, то, с технической точки зрения, после публикации будет сформирован лишь кэш для этой новой страницы. Кэш для домашней страницы при этом окажется устаревшим.
Мы всё ещё ищем способы улучшения системы кэширования нашего проекта. Но до сих пор акцент делался на том, чтобы разобраться с существующими проблемами производительности. Мы полагаем, что проделали достаточно хорошую работу в плане решения этих проблем.
Итоги
Вначале мы применяли клиентский рендеринг. Тогда, в среднем, пользователь мог увидеть страницу через 3.3 секунды. Теперь же этот показатель сократился до примерно 600 мс. Важно ещё и то, что сейчас мы обходимся без индикатора загрузки.
Достичь такого результата нам позволило, в основном, применение серверного рендеринга. Но без хорошей системы кэширования оказывается, что вычисления просто переносят с клиента на сервер. А это приводит к тому, что время, необходимое на то, чтобы пользователь увидел бы страницу, особенно сильно не меняется.
У применения серверного рендеринга есть ещё одно положительное качество, не упомянутое ранее. Речь идёт о том, что он позволяет облегчить просмотр страниц на слабых мобильных устройствах. Скорость подготовки страницы к просмотру на таких устройствах упирается в скромные возможности их процессоров. Серверный рендеринг позволяет снять с них часть нагрузки. Надо отметить, что мы не проводили специального исследования этого вопроса, но та система, которая у нас есть, должна способствовать улучшению и в сфере просмотра сайта на телефонах и планшетах.
В целом же можно сказать, что реализация серверного рендеринга — это непростая задача. А то, что мы пользуемся бессерверной средой, лишь эту задачу усложняет. Решение наших проблем потребовало изменений кода, дополнительной инфраструктуры. Нам понадобилось создать хорошо продуманный механизм кэширования. Но взамен мы получили много хорошего. Самое главное — это то, что страницы нашего сайта теперь загружаются и готовятся к работе гораздо быстрее, чем раньше. Полагаем, нашим пользователям это понравится.
Уважаемые читатели! Пользуетесь ли вы технологиями кэширования и серверного рендеринга для оптимизации своих проектов?