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

Два основных шаблона рендеринга

Классифицировать шаблоны рендеринга можно следующим образом:

  • Рендеринг на стороне клиента.
  • Предварительный рендеринг.

Рендеринг на стороне клиента, как следует из названия, представляет собой рендеринг компонентов в браузере.

Предварительный рендеринг может быть выполнен различными способами:

  • SSG (static site generation — генерирование страниц статически).
  • SSR (server side rendering — рендеринг на стороне сервера).
  • ISR (incremental static regeneration — инкрементное регенерирование статических страниц).
  • PPR (partial pre-rendering — частичный предварительный рендеринг), доступный с Next 14 в качестве экспериментальной функции.

Рассмотрим сначала рендеринг на стороне клиента, а затем разберемся с различными подходами к предварительного рендерингу.

Рендеринг на стороне клиента

Механизм выполнения

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

Как заставить компонент отображаться в браузере?

Просто добавьте директиву “use client” (использовать клиент”) в начало файла.

Получение данных

Получить необходимые данные можно двумя способами:

  • сделать API-вызов к бэкенду Next JS или к внешней конечной точке;
  • сделать обычный вызов функции к серверному компоненту Next JS.

Если бэкенд также написан на Next JS, то доступ к базе данных может быть осуществлен там же.

Преимущества

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

Ограничения

  1. Возможна ощутимая задержка производительности на стороне пользователя из-за задержки отображения контента. В зависимости от объема JS, который необходимо парсить перед получением HTML, присутствующих в коде операций блокировки (если таковые имеются), ограничений устройства пользователя (подключение к интернету, наличие памяти), выполнение браузером операции рендеринга может занять определенное время, в течение которого пользователь будет видеть пустую страницу или страницу, с которой невозможно взаимодействовать.
  1. SEO-краулеры индексируют страницу, просматривая ее контент путем обхода DOM. Они не могут эффективно индексировать страницы, если им приходится ждать загрузки JS, прежде чем они смогут просканировать DOM.

Клиентские компоненты Next JS предварительно отображаются на сервере, то есть HTML для DOM будет сгенерирован на сервере и отправлен в браузер. Функциональность JavaScript (для интерактивности и дополнительных возможностей) будет обрабатываться браузером. Таким образом, гидратация происходит в браузере.

SSG — генерирование страниц статически

Механизм выполнения

HTML в этом случае генерируется во время сборки и хранится в CDN (Content Delivery Network — сеть доставки контента), которая впоследствии, когда пользователь получает доступ к сайту/определенным маршрутам, обслуживает кэшированные версии страниц.

Когда используется?

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

Страница FAQ может быть сгенерирована статически. Страница About (О нас), страницы документации также не нуждаются в частом обновлении и могут быть сгенерированы в процессе сборки.

Как сделать страницу полностью статичной в Next, начиная с 13-й версии (с новой моделью App Router) и далее?

Чтобы сделать страницу полностью статичной (т. е. не позволить Next решать, рендерить ее на сервере или в процессе сборки, а принять решение самостоятельно), экспортируйте конфиг dynamic из соответствующего файла со значением force-static:

export const dynamic = 'force-static';

Генерирование динамических маршрутов и получение данных в процессе сборки

Для этого можно использовать метод новой модели App Router generateStaticParams вместо getStaticProps и getStaticPaths — функций прежней модели Pages Router.

Преимущества

  1. При наличии большого количества страниц, контент которых вряд ли будет часто меняться, их многократный предварительный рендеринг на сервере увеличивает нагрузку на сервер. Генерирование их статически и сохранение в CDN снимает эту нагрузку с сервера.
  1. Контент, легко доступный при загрузке страницы, повышает шансы на более высокий SEO-рейтинг.

Ограничения

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

SSR — рендеринг на стороне сервера

Этот шаблон также называется динамическим рендерингом.

В файле без директив или с директивой “use server” (“использовать сервер”), после первоначальной загрузки страницы на сервер, выполняется последовательный рендеринг на стороне сервера, когда клиент делает запрос к серверу. Этот запрос может быть в виде выборки данных, для которых было отключено кэширование или проведена повторная валидация. Поскольку страница должна регенерироваться в зависимости от действий пользователя или определенных изменений на стороне клиента, она называется динамической.

Механизм выполнения 

HTML загружается на сервер, JS-функции также остаются на сервере. Затем HTML передается клиенту для отображения.

Обратите внимание:

  1. JS в серверном компоненте выполняется на сервере и не передается клиенту. В браузере для этих компонентов нет гидратации.
  1. Безопасно использовать приватные API-ключи в серверных компонентах, потому что они никогда не будут открыты клиенту.
  1. Можно писать код node.js внутри серверного компонента, например код для чтения/записи файла.

Как сделать страницу полностью динамической

export const dynamic = "force-dynamic";

Преимущества

  1. Благодаря тому, что DOM предварительно отображается, его легче обходить и индексировать поисковым системам.
  1. Доступ к данным может быть осуществлен, и любые полученные данные могут быть немедленно использованы для создания DOM (отображения информации о пользователе в DOM) еще до отправки HTML в браузер. Не требуется дополнительного времени для API-вызова бэкенда.
  1. Данные можно повторно валидировать с определенным интервалом, чтобы они всегда оставались актуальными.
  1. Браузеру не нужно выполнять работу по рендерингу компонента, поэтому любые ограничения на стороне клиента, такие как плохое сетевое соединение, нехватка памяти и т. д., не будут препятствовать загрузке страницы.

Ограничения

  1. Серверные компоненты не являются интерактивными, поскольку интерактивность требует использования обработчиков событий, а это возможно только в браузере. Решение проблемы: можно отображать статический контент на стороне сервера, а интерактивный контент — на стороне клиента в виде leaf-компонента. Например, можно иметь одну форму, которая генерируется на стороне клиента, или кнопку, вложенную в страницу с большим количеством контента, которая генерируется на стороне сервера.
  1. Серверные компоненты не могут снабжаться анимацией монтирования/размонтирования, так как для этого необходимо использовать React-хуки, позволяющие узнать, когда компонент смонтирован, а React-хуки недоступны на сервере.
  1. В серверных компонентах невозможно использовать браузерные API, такие как API веб-хранилища (LocalStorage и sessionStorage), WebRTC (API веб-коммуникаций в реальном времени), Geolocation API (для получения данных о местоположении пользователя).

ISR — инкрементное регенерирование статических страниц

Механизм выполнения

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

// каталог `app`
async function getPosts() {
  const res = await fetch(`https://.../posts`, 
  { next: { revalidate: 60 } });
  const data = await res.json();
 
  return data.posts;
}
 
export default async function PostList() {
  const posts = await getPosts();
 
  return posts.map((post) => <div>{post.name}</div>);
}

Метод fetch задает свойство revalidate (повторной валидации) со значением 60. Таким образом, страница будет повторно получать данные и регенерироваться с новыми данными с интервалом в 60 секунд.

Повторная валидация при неиспользовании fetch: если вы используете axios или ORM, например Prisma, то экспортируйте конфиг revalidate с нужным интервалом времени:

export const revalidate = 3600 // повторная валидация не чаще одного раза в час

Преимущества

  • Сокращение времени загрузки и лучшие SEO-показатели у статически сгенерированных страниц с обновленными данными.

PPR — частичный предварительный рендеринг (экспериментальная функция)

Чтобы подключить функцию частичного предварительного рендеринга, нужно:

  • установить пробную (canary) версию Next:
npm install next@cannary

или:

npx create-next-app@latest .
  • после инициализации проекта перейти в файл next.config.js и добавить следующее:
experimental: {
ppr: true
}

Для чего нужен этот шаблон?

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

Например, на странице “Product details” (“Детали продукта”) следующие данные вполне могут быть статическими (сгенерированными во время сборки):

1. Название продукта.

2. Описание продукта.

3. Цена продукта (с возможностью перепроверки, чтобы цена могла быть обновлена).

4. Другая информация о продукте.

При этом некоторые элементы контента должны быть динамическими (генерироваться на сервере по запросу пользователя):

1. Общее количество оценок и отзывов.

2. Рейтинг продукта.

3. Раздел отзывов.

Механизм выполнения

Маршрут отображается в статической оболочке загрузки. Сюда можно добавлять динамические элементы.

При создании приложения Next.js выполняет предварительный рендеринг статической оболочки (static _shell_) для каждой страницы приложения, оставляя окна для динамического контента.

Таким образом, можно весь маршрут “продукт-детали” отобразить в статической оболочке загрузки, а внутри нее создать динамическими счетчик рейтинга, счетчик отзывов, раздел отзывов.

Поскольку динамический контент передается параллельно с async, они не блокируют время загрузки друг друга.

После подключения PPR все предварительно отрендеренные страницы (те, которые не помечены директивой “use client”), будут работать так, как описано выше. То есть компонент будет отображаться внутри загрузочной оболочки, сгенерированной статически.

Затем можно обернуть динамические компоненты в react-suspense-boundaries (встроенный поставщик кэша). Так, вложенный маршрут “продукт-детали” будет содержать изображения продукта, его описание, название, цену, сгенерированные статически, а отзывы, количество отзывов, рейтинг и т. д. будут обернуты внутри React Suspense с соответствующими резервными (fallback) компонентами.

Код, встречая эти компоненты, обернутые в React Suspense, берет предоставленные резервные компоненты, генерирует их статически и приостанавливает генерацию обернутого компонента, пока не появятся данные для него.

Все такие компоненты, обернутые в React Suspense, работают в асинхронном режиме и получают данные параллельно. Когда данные для одного из компонентов становятся доступны, он рендерится.

return (
 <div>
  <A/>
  <B/>
  <Suspense fallback={<FallbackC/>}>
   <C data={fetch async data}/>
  </Suspense>
  <D/>
  <Suspense fallback={<FallbackE/>}>
   <E data={fetch async data}/>
  </Suspense>
  <F/>
 </div>
)

Представленные здесь компоненты A, B, FallbackC, D, FallbackE и F будут сгенерированы статически во время сборки.

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

Поведение рендеринга Next.js по умолчанию

По умолчанию Next JS пытается выбрать SSG (генерирование страниц статически). Поэтому значение переменной dynamic будет установлено на auto, если вы не измените его сами. Это означает, что Next будет пытаться кэшировать все, если только вы явно не укажете не делать этого.

Чтобы сознательно выбрать динамический рендеринг (на сервере), нужно выполнить одно из следующих действий:

1. Экспортировать переменную dynamic со значением force-dynamic.

2. Использовать динамические имена путей, например [id], без применения generateStaticParams.

3. Экспортировать конфиг revalidate со значением 0.

export const revalidate = 0;

4. Использовать динамические функции, такие как cookies() или header().

5. Выполнить fetch-запрос и передать { cache: ‘no-store’ } или { next: { revalidate: 0 } }.

Как определяется место рендеринга

Как вы уже знаете, директивы “use client” и “use server” служат для обозначения компонента, который будет отображаться на клиенте и сервере соответственно. Но как на самом деле интерпретируются эти директивы?

Добавление “use client” в начало файла не только приводит к отображению в браузере компонентов, созданных в этом файле. Все компоненты, от которых зависит файл, также будут отображаться в браузере. Зависимости файла с пометкой “use client” от других компонентов разрешаются путем проверки всех операторов импорта в этом файле.

Рассмотрим приведенный выше сценарий. Вы указали, что компонент Home будет отображаться в браузере, а компонент Testimonials — на сервере. Но сначала загружаются серверные компоненты, а затем клиентские. Вы просите Next JS подождать, пока компонент Home загрузится на стороне клиента, прежде чем рендерить Testimonials на сервере. Но как Next отрендерит компонент Home на стороне клиента, если одна из его зависимостей — компонент Testimonials — еще не был отрисован?

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

Место рендеринга — на стороне клиента или сервера — определяется на основе зависимости файлов друг от друга, которая, в свою очередь, определяется путем проверки операторов импорта в файле.

Вы можете сообщить Next JS о своем предпочтении загрузить компонент на стороне сервера, но если клиентский компонент зависит от него, то он будет отрисован только на стороне клиента.

Решение проблемы

Чтобы компонент Testimonials по-прежнему загружался на сервер, а рендерился из компонента Home, нужно передать Testimonials как дочернее свойство из родительского компонента сервера в компонент Home и рендерить его как дочерний элемент:

Зависимость от Testimonials была перенесена из home.js в page.js, который также является серверным компонентом.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Rupamita Sarkar: Next JS Rendering Patterns — a Comprehensive Guide

Предыдущая статьяВозможности контроля в JavaScript: методы AbortSignal.timeout() и AbortSignal.any()
Следующая статьяИспользование LLM в реальном мире