Серверный рендеринг в бессерверной среде

Автор оригинала: Sven Al Hamad
  • Перевод
Автор материала, перевод которого мы публикуем, является одним из основателей проекта 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 к браузеру. Эти задержки возникают при выполнении каждого запроса.

Задачи оптимизации


Мы, основываясь на вышеприведённом анализе, сформулировали несколько задач, которые нам нужно было решить для оптимизации проекта. Вот они:

  • Улучшение скорости выполнения запросов к API или уменьшение числа запросов к API, которые блокируют рендеринг.
  • Уменьшение размера JS-бандла или перевод этого бандла в состав ресурсов, которые не являются необходимыми для вывода страницы.
  • Разблокировка главного потока.

Подходы к решению задач


Вот несколько подходов к решению задач, которые мы рассматривали:

  1. Оптимизация кода в расчёте на ускорение его выполнения. Этот подход требует больших усилий, он отличается высокой стоимостью. Выгоды, которые можно получить в результате подобной оптимизации, сомнительны.
  2. Увеличение объёма оперативной памяти, доступной функциям AWS Lambda. Сделать это легко, стоимость такого решения находится где-то между средней и высокой. От применения этого решения можно ожидать лишь небольших положительных эффектов.
  3. Применение какого-то другого способа решения задачи. Правда, в тот момент мы ещё не знали о том, что это за способ.

Мы, в итоге, выбрали третий пункт этого списка. Мы рассуждали так: «Что если нам совершенно не будут нужны запросы к API? Что если мы сможем обойтись совсем без JS-бандла? Это позволило бы нам решить все проблемы проекта».


Первой идеей, которая показалась нам интересной, было создание 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 мс. Важно ещё и то, что сейчас мы обходимся без индикатора загрузки.

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

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

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

Уважаемые читатели! Пользуетесь ли вы технологиями кэширования и серверного рендеринга для оптимизации своих проектов?

  • +24
  • 4,8k
  • 5
RUVDS.com
821,62
RUVDS – хостинг VDS/VPS серверов
Поделиться публикацией

Комментарии 5

    0

    2000-е: клиенты медленные, js кривой, давайте рендерить на сервере
    2010-е: серверы медленные, страницы долго качаются и шаблонизатор жрёт проц, давайте рендерить на клиенте
    2020-е: клиенты медленные, бандл долго качается и жрёт проц, давайте рендерить на сервере


    Пользуетесь ли вы технологиями кэширования и серверного рендеринга для оптимизации своих проектов?

    Рендерю всё своё Django-шаблонизатором или Jinja2, кладу в memcached или Redis, инвалидирую по необходимости и полностью доволен жизнью. Зачем связываться со SPA и получать кучу проблем, не понимаю.

      0
      SSR не исключает SPA, он только позволяет сократить time to interactive до приемлемых значений за счёт подготовки базовой страницы до загрузки бандла.
        0

        Я в курсе.


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

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

          0
          Мерцание — это проблема не SSR+SPA, а кривой реализации.
            0

            Ещё лучше, автор поста прямым текстом рекомендует делать кривую реализацию.

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

    Самое читаемое