React Server-Side Rendering (SSR) — руководство новичка
В этом уроке мы поговорим о серверном рендеринге (SSR), его преимуществах и подводных камнях. Затем мы создадим мини React проект и express сервер (Node.js), чтобы продемонстрировать, как можно достичь SSR.
Почти каждый второй сайт на данный момент является одностраничным приложением (SPA). Я уверен вы знаете, что такое одностраничное приложение. Такие фреймворки как Angular, React, Vue, Svelte и т.д. находятся на подъеме из-за их способности быстро и эффективно разрабатывать SPA. Они идеально подходят не только для быстрого прототипирования, но и для разработки сложных веб-приложений (если всё сделано правильно).
До недавнего времени для большинства сайтов HTML генерировался на сервере и отправлялся вместе с ответом, чтобы браузер мог визуализировать его на экране. Всякий раз, когда пользователь нажимал на ссылку для доступа к новой странице, мы отправляли серверу новый запрос на генерацию нового HTML для этой страницы. Нет ничего плохого в этом подходе, кроме времени загрузки и опыта пользователя.
Пользователю приходилось ждать несколько секунд, пока сервер получит запрос, соберет данные, составит HTML и вернет ответ. Так как это было полностраничной загрузкой, браузер должен был ждать все ресурсы, такие как JavaScript, CSS, и другие файлы, чтобы загрузить их снова (если только они не кешируются должным образом). Это было огромным неудобством для пользователя.
В настоящее время большинство веб-приложений запрашивают у сервера только данные. Вся генерация HTML осуществляется на стороне клиента (браузер). Всякий раз, когда пользователь нажимает на ссылку, вместо отправки нового запроса на сервер для получения HTML этой страницы, мы создаем HTML на стороне клиента, монтируя новые компоненты (строительные блоки веб-приложений), и запрашиваем у сервера только те данные, которые нужны для наполнения этого HTML.
Таким образом, мы предотвращаем полную перезагрузку страницы и значительно улучшаем время загрузки страницы. Мы программно изменяем URL в браузере с помощью History API, что не приводит к обновлению браузера. Так как обновление страницы никогда не происходит, мы запрашиваем только начальный HTML, который включает в себя JavaScript, CSS и другие средства для всего приложения.
В приведенном выше HTML,
Давайте поговорим о преимуществах и подводных камнях SPA.
= Преимущества
SPA дают опыт взаимодействия похожий на нативные приложения. Так как весь HTML приложения генерируется на стороне клиента, загрузка страницы происходит очень быстро.
Так как все ресурсы приложения, такие как JavaScript и CSS файлы, загружаются только один раз и никогда не запрашиваются снова после загрузки приложения, мы сильно экономим на пропускной способности сервера.
После начальной загрузки приложения мы запрашиваем у сервера только данные размером в несколько килобайт (по запросу). Таким образом, SPA идеально подходят для использования в условиях плохой сети.
Всё приложение может быть кешировано на клиенте (устройстве) с помощью service worker. Таким образом, при следующем обращении пользователя к сайту, браузеру больше не нужно будет загружать HTML и ресурсы. Когда пользовательское устройство не подключено к интернету, вместо отображения сообщения браузера по умолчанию «Не подключено к интернету!», мы можем отобразить пользовательский экран, который даст пользователю оффлайн-доступ.
Пользователи могут сохранить SPA как приложение на устройстве. Если вы заинтересованы в разработке мобильного приложения но не хотите тратить деньги на разработку нативных приложений (Android или iOS), SPA открывают возможность создать приложение похожее на нативное, используя ту же самую кодовую базу веб-сайта.
= Подводные камни
Так как SPA должен обслуживать все JavaScript и CSS файлы приложения вместе (обычно), эти файлы громоздки (несколько мегабайт). Следовательно, начальная загрузка приложения требует значительно больше времени, что означает, что пользователь будет видеть пустой экран в течение довольно долгого времени. При плохой сети это может быть серьезной проблемой. Однако, мы можем исправить это с помощью SSR.
В SPA, так как JavaScript управляет генерацией HTML на стороне клиента, SPA делают тяжелую работу на стороне клиента, которую раньше выполнял сервер. Поэтому SPA нуждаются в устройствах с хорошими возможностями в части процессора и батареи.
Так как исходный HTML, предоставляемый сервером (для всех страниц) не содержит HTML, специфичный для конкретного применения, поисковый движок видит веб-сайт как пустой, не имеющий никакого содержимого. Таким образом, ваш сайт может не попасть в топ поисковых результатов, несмотря на огромный трафик и релевантное содержание.
Из приведенного выше сравнения видно, что преимущества SPA превосходят их подводные камни. Наши устройства с каждым днем становятся все более производительными, а условия работы сети становятся все лучше и лучше. Поэтому нам больше не нужно беспокоиться об этом.
Тем не менее, каждый сайт хочет занять первое место в результатах поиска. Когда дело доходит до SPA, этого не очень легко достичь. Как мы уже говорили, когда поисковая система (crawler), такая как Google, видит наш веб-сайт, она видит HTML с пустым элементом
Причина, по которой я выделил «при первой загрузке», заключается в том, что этот подход не похож на традиционный рендеринг HTML на стороне сервера всего приложения. Здесь мы генерируем HTML на сервере только один раз для запрашиваемой страницы, так что поисковые системы видят правильный HTML, в то время как приложение будет вести себя в браузере точно так же.
Этот метод имеет еще одно дополнительное преимущество. Так как сервер будет возвращать правильный HTML для страницы, пользователь больше не будет видеть пустой экран, пока все ресурсы приложения не будут загружены. Нам для этого нужно будет настроить несколько параметров, но зато это будет очень удобно для пользователя.
Server-Side Rendering с нуля до профи
Проблема
Главной проблемой Single Page приложений является то, что сервер отдает клиенту пустую HTML страницу. Её формирование происходит только после того как весь JS будет скачан (это весь ваш код, библиотеки, фреймверк). Это в большинстве случаев более 2-х мегабайт размера + задержки на обработку кода.
Даже если Google-бот умеет выполнять JS, он получает контент только спустя некоторое время, критичное для ранжирования сайта. Google-бот попросту видит пустую страницу несколько секунд! Это плохо!
Google начинает выдавать красные карты если ваш сайт рендерится более 3-х секунд. First Contentful Paint, Time to Interactive — это метрики которые будут занижены при Single Page Application. Подробнее читайте здесь.
Также есть менее продвинутые поисковые системы, которые попросту не умеют работать с JS. В них Single Page Application не будут индексироваться.
На ранжирование сайта еще влияет множество факторов, часть из них мы разберем далее в этой статье.
Рендеринг
Существует несколько путей как решить проблему пустой странички при загрузке, рассмотрим несколько из них:
Static Site Generation (SSG). Сделать пререндер сайта перед тем как его загрузить на сервер. Очень простое и эффективное решение. Отлично подходит для простых веб страничек, без взаимодействия с backend API.
Server-Side Rendering (SSR). Рендерить контент в рантайме на сервере. При таком подходе мы сможем делать запросы backend API и отдавать HTML вместе с необходимым содержимым.
Server-Side Rendering (SSR)
Рассмотрим подробнее, как работает SSR:
Из вышеописанной работы SSR приложения мы можем выделить проблемы:
Это маленькая библиотека которая способна решить проблемы асинхронной обработки запросов к данным а также синхронизации состояния с сервера на клиент. Это не “очередной убийца Next.JS”, нет! Next.JS прекрасный фреймверк имеющий множество возможностей, но чтобы его использовать вам придется почти полностью переписать ваше приложение и следовать правилам Next.JS.
Посмотрим на примере, как просто перенести обычное SPA приложение на SSR.
К примеру, у нас есть простейшее приложение с асинхронной логикой.
Данный код рендерит список выполненных задач, используя сервис jsonplaceholder для эмуляции взаимодействия с API.
Сделаем данное приложение SSR!
Шаг 1. Установка зависимостей
Для установки iSSR нужно выполнить:
Для настройки базовой билд системы установим:
Один из неочевидных моментов разработки SSR приложений является то, что некоторые API и библиотеки могут работать на клиенте но не работать на сервере. Одним из таких API является fetch. Данный метод отсутствует в nodejs где будет выполняться серверная логика нашего приложения. Для того, чтобы у нас приложение работало одинаково установим пакет:
Для сервера будем использовать express, но это не важно, можно использовать любой другой фреймверк:
Добавим модуль для сериализации состояния приложения на сервере:
Шаг 2. Настройка webpack.config.js
Шаг 3. Модификация кода
Вынесем общую логику нашего приложения в отдельный файл App.jsx. Это нужно для того, чтобы в файлах client.jsx и server.jsx осталась только логика рендеринга, ничего больше. Таким образом весь код приложения у нас будет общий.
Мы поменяли стандартный render метод React на hydrate, который работает для SSR приложений.
В коде сервера обратите внимание, что мы должны расшаривать папку с собранным SPA приложением webpack:
app.use(express.static(‘public’));
Таким образом, полученный с сервера HTML будет работать далее как обычный SPA
Шаг 4. Обработка асинхронности
Мы разделили логику вынеся общую часть, подключили компилятор для клиент и сервер частей приложения. Теперь решим остальные проблемы связанные с асинхронными вызовами и состоянием.
Для обработки асинхронных операций их нужно обернуть в хук useSsrEffect из пакета @issr/core:
В server.jsx заменим стандартный renderToString на serverRender из пакета @issr/core:
Если запустить приложение сейчас, то ничего не произойдет! Мы не увидим результата выполнения асинхронной функции getTodos. Почему так происходит? Мы забыли синхронизировать состояние. Давайте исправим это.
В App.jsx заменим стандартный setState на useSsrState из пакета @issr/core:
Внесем изменения в client.jsx для синхронизации состояния переданного с сервера на клиент:
window.SSR_DATA — это объект, переданный с сервера с кешированнным состоянием, для синхронизации на клиенте.
Сделаем передачу состояние на сервере:
Обратите внимание, что функция serverRender передает не только HTML, но и состояние, которое прошло через useSsrState, мы его передаем на клиент, в качестве глобальной переменной SSR_DATA. На клиенте, данное состояние будет автоматически синхронизировано.
Шаг 5. Билд скрипты
Осталось добавить скрипты запуска в package.json:
Redux и прочие State Management библиотеки
iSSR отлично поддерживает разные state management библиотеки. В ходе работы над iSSR я заметил, что React State Management библиотеки делятся на 2 типа:
Мы не будем рассматривать этот пример так детально как предыдущий. Ознакомится с полным кодом можно по ссылке.
Redux Saga
*Для лучшего понимания происходящего, читайте предыдущую главу
Сервер запускает наше приложение через serverRender, код выполняется последовательно, выполняя все операции useSsrEffect.
Концептуально Redux работая с сагами не выполняет никаких асинхронных операций. Наша задача отправить action для старта асинхронной операции в слое Саг, отдельных от нашего react-flow. В примере по ссылке выше, в контейнере Redux мы выполняем
Это не асинхронная операция! Но iSSR понимает, что что то произошло в системе. iSSR будет идти по остальным React компонентам выполняя все useSsrEffect если таковые будут и по завершению iSSR вызовет каллбек:
Таким образом мы можем обрабатывать асинхронные операции не только на уровне с React но и на других уровнях, в данном случае мы в начале поставили на выполнение нужные нам саги, после чего в callback serverRender запустили и дождались их окончания.
Я подготовил много примеров использования iSSR, вы можете их найти по ссылке.
SSR трюки
На пути в разработке SSR приложений существуют множество проблем. Проблема асинхронных операций это только одна из них. Давайте посмотрим на другие распространенные проблемы.
HTML Meta Tags для SSR
Немаловажным аспектом в разработке SSR является использование правильных HTML meta tags. Они сообщают поисковому боту ключевую информацию на странице.
Для реализации данной задачи рекомендую использовать один из модулей:
React-Helmet-Async
React-Meta-Tags
Я подготовил несколько примеров:
React-Helmet-Async
React-Meta-Tags
Dynamic Imports
Чтобы снизить размер финального бандла приложения, принято приложение делить на части. Например dynamic imports webpack позволяет автоматически разбить приложение. Мы можем вынести отдельные страницы в чанки или блоки. При SSR мы должны уметь обрабатывать данные фрагменты приложения как одно целое. Для этого рекомендую использовать замечательный модуль @loadable
Dummies
Некоторые компоненты или фрагменты страницы можно не рендерить на сервере. Например, если у вас есть пост и комментарии, не целесообразно обрабатывать обе асинхронные операции. Данные поста более приоритетны чем комментарии к нему, именно эти данные формируют SEO нагрузку вашего приложения. По этому мы можем исключать не важные части при помощи проверок типа:
localStorage, хранение данных
NodeJS не поддерживает localStorage. Для хранения сессионных данных мы используем cookie вместо localStorage. Файлы cookie отправляются автоматически по каждому запросу. Файлы cookie имеют ограничения, например:
React Server Components
React Server Components возможно будет хорошим дополнением для SSR. Его идеей является снижение нагрузки на Bundle за счет выполнения компонент на сервере и выдачи готового JSON React дерева. Нечто подобное мы видели в Next.JS. Читайте подробнее по ссылке
Роутинг
React Router из коробки поддерживает SSR. Отличие в том, что на server используется StaticRouter с переданным текущим URL, а на клиенте Router определяет URL автоматически при помощи location API. Пример
Debugging
Дебаг на сервере может выполняться также как и любой дебаг node.js приложений через inpsect.
Для этого нужно добавить в webpack.config для nodejs приложения:
devtool: ‘source-map’
А в настройки NodemonPlugin:
Также, для улучшения работы с source map можно добавить модуль
В nodeArgs опций NodemonPlugin добавить:
‘—require=«source-map-support/register»’
Пример
Next.JS
Если вы создаете приложение с нуля, рекомендую обратить внимание на данный фреймверк. Это самое популярное решение на данный момент для создания с нуля приложений с поддержкой SSR. Из плюсов можно выделить то, что все идет из коробки (билд система, роутер). Из минусов — необходимо переписывать существующее приложение, использовать подходы Next.JS.
SEO это не только SSR!
Критерии Google бота для SEO оптимизации включают множество метрик. Рендер данных, получение первого байта и т.д. это лишь часть метрик! При SEO оптимизации приложения необходимо минимизировать вес картинок, бандла, грамотно использовать HTML теги и HTML мета теги и прочее.
Для проверки вашего сайта при SEO оптимизации можно воспользоваться:
SSR: когда, зачем и для чего. На примере Vue
(Иллюстрация)
Once upon a time Несколько лет назад, когда я только начинал работать с вебом на Java, мы работали с JSP. Вся страница генерировалась на сервере и отправлялась клиенту. Но потом встал вопрос о том, что ответ приходит слишком долго…
Мы начали использовать подход, при котором отдается пустой темплейт страницы, а все данные уже постепенно подгружались Аяксом. Все были счастливы, странички показывались. Пока мы не поняли, что наделали себе за шиворот, так как CSR отрицательно сказывается на поисковой оптимизации и производительности на мобильных устройствах. Но потом я снова услышал про поддержку SSR JS-фреймворками.
И что же получается, история повторяется?
Какие есть принципы работы SSR?
1. Prerendering. В простейшем случае генерируется N HTML-файлов, которые кладутся на сервер и возвращаются как есть — то есть возвращается статика, во время запроса мы ничего не генерируем.
2. Как и в случае с JSP, на сервере генерируется полный HTML со всем контентом и возвращается клиенту. Но, чтобы не генерировать страницу на каждый запрос (коих может быть миллион и наш сервер загнется), давайте добавим кэш прокси. Например, варниш.
Когда может быть применим каждый из этих способов:
1. Когда имеет смысл генерировать пачку HTML-файлов? Очевидно, в том случае, когда данные на сайте меняются чуть реже чем никогда. Например, корпоративный сайт ларька по ремонту обуви, что за углом (да-да, тот дяденька, который меняет набойки в ларьке 2х2 метра, тоже захотел сайт фирмы — и, конечно же, со страницей миссии компании). Для такого сайта вообще не надо заморачиваться на предмет фреймворков, SSR и прочих свистелок, но это сферический пример. Что делать, если у нас блог, в котором 1к постов? Иногда мы их актуализируем, иногда добавляем новые. Сгенерировать 1к+ статичных файлов… Что-то не то. А если мы изменяем пост, то надо перегенерировать определенный файлик. Хм…
2. И вот тут нам подходит второй способ. Где мы генерируем первый раз на лету, а потом кэшируем ответ в проксирующем сервисе. Время кэширования может быть час/два/день — как угодно. Если у нас 10 000 заходов в час на пост (невероятно, правда?), то только первый запрос дойдет до сервера. Остальные получат в ответ кэшированную копию, и наш сервер с большей вероятностью будет жить. В случае обновления какого-то поста нам просто нужно сбросить закэшированную запись, чтобы по следующему реквесту сгенерировалась уже актуальная страница.
От слов к делу:
0) generate hello world
Для быстрого старта сообщество Nuxt подготовило базовые темплейты, установить любой из них можно командой:
По умолчанию предлагается started-template, его и возьмем для нашего примера. Хотя в реальном приложении мы выбрали express-template. Назовем проект незамысловато:
Вжух, мы сгенерировали hello world. Перейдя по урлу, можно увидеть сгенерированную страницу:
1) Webpack и Linting
Nuxt из коробки имеет настроенные вебпак с поддержкой ES6 (babel-loader), Vue однофайловые компоненты (vue-loader), а также SCSS, JSX и прочее.
Если этих возможностей недостаточно, конфиг вебпака можно расширить. Идем в nuxt.config.js, и в build.extend мы имеем возможность модифицировать конфиг.
Для примера добавим линтинг стилей по аналогии с линтингом кода — важный, на наш взгляд, пункт, для поддержания единообразной кодовой базы. Это хорошая привычка, которая поможет избежать многих подводных камней.
Пример расширения конфига (подключение конфиг-файла для дева на основе переменной окружения):
Остальные изменения можно посмотреть в репо по тегу, эти изменения помогут нам держать стили в порядке.
И пример конфиг-файла линтера: используем Standard JS, как общепринятое в Vue/Nuxt решение:
2) Для примера работы с данными будем использовать вот это API:
Подключим Axios как плагин, создаем новый файл в директории plugins:
И пример использования:
Остальное в репе по тегу.
Второй реквест с фастли
Пустая страница пришла быстро, но потребовалось 2 секунды на то, чтобы сгенерировать на ней контент.
Что такое рендеринг на стороне сервера и нужен ли он мне?
В новом году начнем общение с вами с затравочной статьи о серверном рендеринге (server-side rendering). В случае вашей заинтересованности возможна более свежая публикация о Nuxt.js и дальнейшая издательская работа в этом направлении
С появлением современных фреймворков и библиотек для JavaScript, которые предназначены, прежде всего, для создания интерактивных веб-страниц и одностраничных приложений, сильно изменился весь процесс показа страниц пользователю.
До пришествия приложений, полностью генерируемых на JS в браузере, HTML-разметка выдавалась клиенту в ответ на HTTP-вызов. Это могло происходить путем возврата статического HTML-файла с контентом, либо путем обработки отклика при помощи какого-либо серверного языка (PHP, Python или Java), причем, более динамическим образом.
Подобное решение позволяет создавать отзывчивые сайты, работающие гораздо быстрее стандартных сайтов, действующих по модели «запрос-отклик», поскольку при работе устраняется время, проводимое запросом «в пути».
Типичный отклик, отправляемый сервером на запрос к сайту, написанному на React, будет выглядеть примерно так:
В этот момент уже можно воспользоваться встроенным в браузер инспектором HTML, чтобы просмотреть весь отображенный HTML. Однако, заглянув в исходный код, мы не увидим ничего кроме HTML, приведенного выше.
Почему это проблема?
Хотя такое поведение и не доставит проблем большинству наших пользователей либо при разработке приложения, оно может стать нежелательным, если:
“Ладно, но в демографическом отношении моя целевая аудитория точно не относится ни к одной из этих групп, так стоит ли мне волноваться?”
Есть еще две вещи, которые следует учитывать при работе с приложением, рендеринг в котором выполняется на стороне клиента: поисковики и присутствие в социальных сетях.
Сегодня из всех поисковиков лишь Google обладает некоторыми возможностями отобразить и сайт и учесть его JS прежде, чем отобразить страницу. Кроме того, хотя Google и сможет отобразить индексную страницу вашего сайта, известно, что могут возникать проблемы с навигацией по сайтам, на которых реализован роутер.
Это означает, что вашему сайту будет очень непросто забраться в топ выдачи любого поисковика кроме Google.
Та же проблема просматривается и в социальных сетях, например, в Facebook — если ссылкой на ваш сайт поделятся, то ни его название, ни картинка-превью не отобразятся как следует.
Как решить эту проблему
Есть несколько способов ее решения.
A — Попробуйте оставить все ключевые страницы вашего сайта статическими
Когда создается сайт-платформа, куда пользователю придется входить под своим логином, а без входа в систему контент посетителю не предоставляется, можно попробовать оставить статическими (написанными на HTML) общедоступные страницы вашего сайта, в частности, индекс, «о нас», «контакты» и не использовать JS при их отображении.
Поскольку ваш контент ограничен требованиями по входу в систему, он не будет индексироваться поисковиками, и им нельзя будет поделиться в социальных сетях.
B — Генерируйте части вашего приложения в виде HTML-страниц в процессе сборки
В проект можно добавить такие библиотеки как react-snapshot; они используются для генерации HTML-копий страниц вашего приложения и для сохранения их в специально предназначенном каталоге. Затем этот каталог развертывается наряду с пакетом JS. Таким образом, HTML будет подаваться с сервера вместе с откликом, и ваш сайт увидят в том числе те пользователи, у которых отключен JavaScript, а также заметят поисковики и т.д.
Как правило, сконфигурировать react-snapshot не составляет труда: достаточно добавить библиотеку в ваш проект и изменить сборочный скрипт следующим образом:
C — Создать на JS приложение, использующее серверный рендеринг
Один из важнейших выигрышных моментов у современного поколения приложений на JS заключается в том, что их можно запускать как на клиенте (в браузере), так и на сервере. Так удается генерировать HTML для страниц, являющихся более динамичными, таких, чей контент в период сборки еще не известен. Подобные приложения часто называют «изоморфными» или «универсальными».
Два наиболее популярных решения, обеспечивающих серверный рендеринг для React:
Создайте собственную реализацию SSR
Важно: если вы собираетесь попробовать самостоятельно создать собственную реализацию SSR для приложений на React, то должны будете обеспечить работу node-бэкенда для вашего сервера. Вы не сможете развернуть это решение на статическом хосте, как в случае со страницами github.
Первым делом нам понадобится создать приложение, точно как в случае с любым другим приложением React.
Давайте создадим входную точку:
И компонент-приложение (App):
А также “оболочку”, чтобы загрузить наше приложение:
Express – это мощный веб-сервер для node, pug – движок-шаблонизатор, который можно использовать с express, а babel-node – это обертка для node, обеспечивает транспиляцию на лету.
Сначала скопируем наш файл index.html и сохраним его как index.pug :
Создадим наш сервер:
Разберем этот файл по порядку.
При помощи renderToString мы отображаем наше приложение. Код выглядит точно как у входной точки, но такое совпадение не является обязательным.
Теперь, когда у нас есть отображенный HTML, мы приказываем express отобразить в ответ файл index.pug и заменить переменную app тем HTML, что мы получили.
Наконец, мы обеспечиваем запуск сервера и настраиваем его так, чтобы он слушал порт 3000.
Теперь нам осталось всего лишь добавить нужный скрипт в package.json :
Если все выглядит именно так, это означает, что серверный рендеринг работает как положено, и можно приступать к расширению вашего приложения!
Зачем же нам по-прежнему нужен bundle.js?
В случае такого крайне простого приложения, которое рассмотрено здесь, включать bundle.js не обязательно – без этого файла наше приложение все равно останется работоспособным. Но в случае с реальным приложением включить этот файл все-таки потребуется.
Это позволит браузерам, умеющим обрабатывать JavaScript, взять работу на себя и далее взаимодействовать с вашей страницей уже на стороне клиента, а тем, что не умеют разбирать JS – перейти на страницу с нужным HTML, который возвратил сервер.
О чем необходимо помнить
Притом, что серверный рендеринг выглядит достаточно незамысловато, при разработке приложений нужно обращать внимание на некоторые темы, на первый взгляд не вполне очевидные: