Стратегии рендеринга: SSG, SSR, ISR и CSR
Каждый фреймворк серверного рендеринга отвечает на один вопрос: в какой момент времени HTML страницы готовится к показу пользователю? В Next.js ответ зависит от комбинации трёх факторов: что написано в коде (fetch с no-store или с revalidate), как устроен маршрут (статический или динамический) и где кэшируется результат (на сервере, на CDN или в браузере).
В краткой версии учебника ты видел таблицу SSG/SSR/ISR/Streaming. Здесь мы идём глубже: разбираем, как Next.js принимает решение о стратегии, как устроены кэши маршрутов и почему «пользователь видит старые данные» — это почти всегда вопрос неправильно выбранной стратегии, а не баг фреймворка. Точка входа в официальную документацию — Static and Dynamic Rendering.
Представь реальный продакшен-сценарий. У тебя маркетплейс: главная страница с баннерами, каталог с фильтрами и личный кабинет с балансом. Если отрендерить всё через SSR — каждый запрос будет бить в базу, а CDN не сможет закэшировать ничего. Если всё через SSG — баланс пользователя будет показывать чужие данные из общего кэша. Правильный ответ: три разные стратегии для трёх типов страниц, и Next.js позволяет смешивать их в рамках одного приложения, вплоть до одного маршрута.
Четыре стратегии: когда какая
Заголовок раздела «Четыре стратегии: когда какая»SSG — статическая генерация при сборке
Заголовок раздела «SSG — статическая генерация при сборке»HTML генерируется один раз на этапе next build и раздаётся как статика с CDN. Самый быстрый вариант: TTFB измеряется в миллисекундах, нагрузка на сервер — ноль.
// app/blog/[slug]/page.tsx — статическая страницаexport async function generateStaticParams() { // на сборке Next.js вызовет эту функцию и сгенерирует // по одной HTML-странице на каждый slug const posts = await getAllPosts(); return posts.map((post) => ({ slug: post.slug }));}
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const post = await getPost(slug); // fetch кэшируется при сборке return <article>{/* ... */}</article>;}Полный API функции — в справочнике: generateStaticParams.
Когда использовать: блоги, документация, лендинги, страницы условий — всё, что одинаково для всех пользователей и меняется редко.
Ограничение: данные на момент сборки. Если пост опубликовался после деплоя — страница его не знает, пока не будет новый билд или ISR.
SSR — рендеринг на каждый запрос
Заголовок раздела «SSR — рендеринг на каждый запрос»HTML собирается заново при каждом запросе. Дороже, зато данные всегда свежие и можно персонализировать под конкретного пользователя.
// app/dashboard/page.tsx — серверный рендеринг на каждый запросexport const dynamic = 'force-dynamic'; // запрещаем любое кэширование маршрута
export default async function Dashboard() { const session = await getSession(); // cookies() делает маршрут динамическим const orders = await getOrders(session.userId); // свежие данные на каждый запрос return <DashboardView user={session.user} orders={orders} />;}Когда использовать: личный кабинет, корзина, админки — всё, что зависит от сессии и должно быть актуальным прямо сейчас.
ISR — инкрементальная статическая регенерация
Заголовок раздела «ISR — инкрементальная статическая регенерация»Гибрид: HTML генерируется статически, но Next.js в фоне пересобирает его с заданной периодичностью. Пользователь всегда получает быстрый ответ из кэша; свежесть данных — с задержкой до одного интервала ревалидации.
// app/products/page.tsx — ISR с ревалидацией раз в 5 минутexport const revalidate = 300; // секунд
export default async function ProductsPage() { const products = await getProducts(); // кэш сбрасывается каждые 5 минут return <ProductGrid products={products} />;}Когда использовать: каталоги, ленты новостей, SEO-страницы категорий — контент, который меняется регулярно, но допустима задержка в минуты.
Механика под капотом: первый запрос после истечения revalidate получает старый HTML из кэша, а Next.js параллельно запускает регенерацию. Следующие запросы — уже новый HTML. Это называется stale-while-revalidate (см. гайд по ISR), и это ключевое свойство ISR: пользователь никогда не ждёт пересборки.
CSR — клиентский рендеринг
Заголовок раздела «CSR — клиентский рендеринг»HTML — пустой каркас, весь рендеринг происходит в браузере. В App Router это не отдельная «стратегия страницы», а состояние отдельных клиентских компонентов внутри серверной страницы. Чистый CSR нужен редко: админские интерфейсы с тяжёлыми таблицами, страницы за логином, где SEO не важно, а интерактивность — всё.
Как Next.js решает: статика или динамика
Заголовок раздела «Как Next.js решает: статика или динамика»Next.js анализирует маршрут на этапе сборки и при выполнении. Маршрут становится динамическим (SSR), если внутри дерева серверных компонентов встречается хотя бы одно из:
| Функция / API | Почему делает маршрут динамическим |
|---|---|
cookies(), headers() |
Зависит от конкретного запроса |
searchParams в page |
Параметры запроса меняют результат |
fetch(..., { cache: 'no-store' }) |
Данные должны быть свежими |
| Не-cached вызов БД напрямую | Next.js не может доказать воспроизводимость |
export const dynamic = 'force-dynamic' |
Явный запрет статики |
Неизвестный динамический сегмент без generateStaticParams |
Слуг не было на сборке — рендерим по запросу |
Обратная операция — export const dynamic = 'force-static' — принудительно делает маршрут статическим (применимо не всегда: если код реально читает cookies, сборка упадёт).
// app/catalog/page.tsx — принудительная статика, даже если что-то подозревает динамикуexport const dynamic = 'force-static';export const revalidate = 3600;Два кэша маршрутов: Full Route Cache и Router Cache
Заголовок раздела «Два кэша маршрутов: Full Route Cache и Router Cache»Это место, где путаются чаще всего. У Next.js два разных кэша на уровне маршрутов, и они живут в разных местах.
Full Route Cache (сервер)
Заголовок раздела «Full Route Cache (сервер)»Серверный кэш готового RSC payload и HTML для статических и ISR-маршрутов (подробная схема — в официальном гайде по кэшированию). Лежит на сервере (в памяти standalone-сервера или на CDN при кэширующем прокси). Ключ — путь маршрута. Сбрасывается через revalidatePath, revalidateTag или истечение revalidate.
Router Cache (клиент)
Заголовок раздела «Router Cache (клиент)»Браузерный кэш RSC payload для prefetch и soft-навигации. Когда ты кликаешь по <Link> внутри приложения, Next.js не запрашивает HTML, а запрашивает RSC payload и кладёт его в память клиента. Ключи особенности:
- автоматический prefetch: Next.js префетчит видимые ссылки (
viewportвnext.config); - кэш живёт в
sessionStorage-подобном хранилище на время сессии; - статические маршруты кэшируются на 5 минут, динамические — на 30 секунд;
- он не сбрасывается при
revalidatePath—router.refresh()сбрасывает принудительно.
'use client';import { useRouter } from 'next/navigation';
export function RefreshButton() { const router = useRouter(); // после мутации вне Server Action — забираем свежий RSC payload return <button onClick={() => router.refresh()}>Обновить данные</button>;}Сравнение по TTFB и персонализации
Заголовок раздела «Сравнение по TTFB и персонализации»| Стратегия | TTFB | Персонализация | Свежесть данных | Нагрузка на сервер |
|---|---|---|---|---|
| SSG | ~0 мс (CDN) | Нет | На момент сборки | Минимум |
| ISR | ~0 мс (CDN) | Нет | С задержкой до revalidate |
Низкая (регенерация в фоне) |
| SSR | 50–500 мс | Полная | Мгновенная | Высокая, на каждый запрос |
| Streaming SSR | ~0 мс за shell | Полная | Мгновенная | Высокая, но отдача постепенная |
| CSR | ~0 мс за shell + ожидание JS | Полная | После загрузки JS | Низкая на сервере, цена — у пользователя |
Правило выбора: бери самую «статичную» стратегию, которую позволяет бизнес-требование. Личный кабинет не может быть SSG — ок. Но страница «О компании» не должна быть SSR «по привычке»: это прямой ущерб TTFB и нагрузке на сервер, а значит — и деньгам на инфраструктуре.
Гибриды: статика, динамика и островки в одной странице
Заголовок раздела «Гибриды: статика, динамика и островки в одной странице»Стратегии не обязаны жить на уровне целых страниц — они комбинируются внутри одного маршрута. Три рабочих паттерна:
Статический shell + динамические островки. Страница собирается статически (SSG/ISR), а персональные данные подгружаются клиентским компонентом или через серверный компонент с no-store внутри Suspense-границы:
// app/product/[id]/page.tsx — ISR-каркасexport const revalidate = 300;
export default async function ProductPage({ params }: Props) { const { id } = await params; const product = await getProduct(id); // ISR-данные return ( <main> <ProductInfo product={product} /> {/* статика */} <Suspense fallback={<PriceSkeleton />}> <PersonalPrice productId={id} /> {/* no-store: цена для конкретного юзера */} </Suspense> </main> );}Динамический layout + статичные дети. Наоборот: каркас страницы зависит от сессии (динамический layout), а вложенные сегменты — статика. Работает: динамичность родителя не «заражает» детей, если те не используют динамические API.
Streaming как мост. Тяжёлый динамический блок не должен блокировать статический: оборачивай в <Suspense> (подробности — в главе про streaming).
Типичные ошибки и грабли
Заголовок раздела «Типичные ошибки и грабли»-
ISR вместо точечной ревалидации.
revalidate = 60на всём каталоге опрашивает данные зря. Лучше длинный интервал (илиfalse) +revalidateTag('products')из Server Action при реальном изменении. Плохо: таймер «на всякий случай». Хорошо: событийная инвалидация. -
Смешение кэшей в голове. «Обновил данные, а страница старая» — проверь, что именно старое: Full Route Cache (сервер, решается
revalidatePath), Router Cache (браузер, решаетсяrouter.refresh()) или Data Cache (отдельный слой, решаетсяrevalidateTag). Три разных кэша — три разных решения. -
Динамический маршрут «случайно». Вызвал
headers()в layout корня — вся страница стала SSR, и SSG-страницы перестали собираться статикой. Проверяй выводnext build: колонка ○ (static) / ƒ (dynamic) показывает реальное решение фреймворка. -
generateStaticParamsбез fallback-стратегии. Если slug появился после сборки, приdynamicParams = falseбудет 404, по умолчанию — SSR для нового slug (это хорошо). Понимай разницу междуdynamicParams = false(только то, что собрано) и дефолтным поведением (ISR-подобно для новых slug’ов). -
SSR «по умолчанию» из страха устаревших данных. Команда ставит
force-dynamicвсему, «чтобы не думать». Итог: база падает от нагрузки, CDN бесполезен, TTFB вырос вдесятеро. Думать о стратегии — обязанность инженера, а не опциональная оптимизация. -
Персонализация на SSG-страницах через клиентский фетч. Страница статическая, а имя пользователя догружается в
useEffect— мигание контента, layout shift, лишний запрос. Правильно: статический shell + клиентский островок сuseUser(), или вообще динамический сегмент лейаута.
Вопросы на собеседовании
Заголовок раздела «Вопросы на собеседовании»- Чем Full Route Cache отличается от Router Cache? Full Route Cache — серверный кэш HTML/RSC payload для статических и ISR маршрутов, сбрасывается ревалидацией. Router Cache — клиентский кэш RSC payload для soft-навигации, живёт в браузере и не сбрасывается серверной ревалидацией.
- Как Next.js решает, рендерить ли маршрут статически? Анализирует код на наличие динамических API:
cookies(),headers(),searchParams,no-storefetch. Если ни одного — маршрут статический; решение видно в выводеnext build. - Объясни stale-while-revalidate в ISR. Первый запрос после истечения интервала получает старый HTML из кэша, а регенерация идёт в фоне; следующие запросы получают свежий. Пользователь никогда не ждёт сборки страницы.
- Нужна страница с балансом пользователя. Какая стратегия и почему? SSR (или динамический рендер): данные персональные и должны быть актуальными. SSG невозможен из-за персонализации, ISR — рискованно из-за денег, CSR — плохой UX и лишний запрос.
- Пользователь жалуется на устаревший контент после публикации. Что проверяешь? Три кэша: Data Cache (теги/время fetch), Full Route Cache (путь), Router Cache (браузер). Дальше — какой именно слой устарел и какой способ ревалидации применялся.
- Что такое
generateStaticParamsи когда без него обходятся? Функция, возвращающая список параметров динамического сегмента для сборки статических страниц. Без неё страницы рендерятся по запросу (ISR-подобно). Нужна, когда полный список slug’ов известен заранее. - Может ли один маршрут быть частично SSG и частично SSR? Да: статический лейаут + динамические вложенные сегменты, либо статическая страница с динамическими островками, либо Streaming с статическим shell и динамическими Suspense-блоками.
Практика
Заголовок раздела «Практика»- В pet-проекте раздели три маршрута:
/about(SSG),/products(ISR, 300 секунд),/account(SSR через чтение cookies). Прогониnext buildи зафиксируй вывод: какие маршруты помечены ○, какие ƒ. - Настрой
generateStaticParamsдля/blog/[slug]с пятью постами. Открой страницу нового поста, которого не было на сборке, и объясни поведение. Затем добавьexport const dynamicParams = falseи сравни. - Сделай страницу
/time: серверный компонент выводит текущее время,cache: 'no-store'. Замерь TTFB черезcurl -w "@curl-format.txt". Повтори с SSG-версией и сравни цифры. - Смоделируй проблему Router Cache: создай страницу со значением из Data Cache, обнови значение через Server Action с
revalidatePath, вернись на страницу soft-навигацией — и прямой ссылкой. Задокументируй разницу и добавьrouter.refresh().
Критерий результата: ты можешь по выводу next build предсказать, как закэшируется каждый маршрут, и объяснить, какие три кэша участвуют в отдаче страницы.