open atlas
↑ К треку
Next.js с нуля до senior NEXT · 01 · 01

SSG, SSR, ISR: кто собирает HTML и когда

SSG собирает HTML при деплое, SSR — на каждый запрос, ISR отдаёт устаревшую страницу и перестраивает её в фоне после окна revalidate. Ось выбора — TTFB против свежести и стоимости инфраструктуры; классические сбои — stale после деплоя и лавина одновременных ревалидаций.

NEXT Middle ◷ 18 min
Уровень
ОсновыJuniorMiddleSenior
Уже знаешь этот юнит? Пройди быструю проверку за минуту →

Чёрная пятница, 09:00. Маркетинг включает флаг скидок в CMS и ждёт, когда главная покажет новые цены. Ничего. Через десять минут — всё ещё старые цены, но только у части пользователей. Команда передеплоивает «чтобы сбросить кеш», и становится хуже: несколько минут часть товарных страниц отвечает 500, потому что десять тысяч запросов одновременно бьют в страницу, чей кеш только что исчез, и каждый из них запускает полный рендер против CMS с лимитом 50 req/s. Никто в команде не смог ответить на единственный важный вопрос: для этой страницы — кто собирает HTML и когда? Главная была на ISR с окном revalidate в 15 минут, поэтому изменение в CMS оставалось невидимым, пока кто-то не попадал на страницу после истечения окна — в каждом регионе CDN отдельно. Деплой не «сбросил кеш» — он опустошил кеш ISR целиком, превратив каждый первый запрос в сборку на лету. Это не баг. Это стратегия рендеринга, которую никто осознанно не выбирал.

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

Три ответа на один вопрос

Любая стратегия рендеринга — это ответ на один вопрос: в какой момент сервер превращает ваше React-дерево в HTML? SSG (Static Site Generation) отвечает «во время сборки»: next build рендерит страницу один раз, результат — обычный .html-файл (плюс полезная нагрузка для клиентской навигации), и CDN отдаёт его вечно: TTFB — это скорость ближайшего edge-узла, обычно 10–50 мс, а origin не делает на запрос вообще ничего. SSR (Server-Side Rendering) отвечает «на каждый запрос»: каждый посетитель платит за свежий рендер — сервер забирает данные, исполняет компоненты и стримит HTML. Вы получаете идеальную свежесть и доступ к контексту запроса (cookies, заголовки, гео), а взамен в TTFB въезжает весь ваш слой данных: страница, ждущая запрос к базе на 200 мс, имеет пол TTFB не ниже 200 мс — умноженный на каждого конкурентного пользователя. ISR (Incremental Static Regeneration) — осознанный компромисс: отдавать закешированную статическую страницу немедленно, даже устаревшую, а если она старше своего окна revalidate — запустить перерендер в фоне. Посетитель, который запустил регенерацию, всё равно получает старую страницу; новую увидит следующий. Это паттерн stale-while-revalidate, применённый к целым страницам.

В App Router режим не выбирается флагом конфигурации — поведение маршрута вытекает из того, как он получает данные:

// app/products/[id]/page.tsx

// ISR: страница статическая, перерендеривается в фоне
// не чаще раза в 60 секунд на каждый путь.
export const revalidate = 60;

export async function generateStaticParams() {
  // Топ-100 товаров пререндерим при сборке (SSG);
  // остальные id рендерятся при первом запросе и кешируются (ISR on demand).
  const top = await getTopProducts(100);
  return top.map((p) => ({ id: p.id }));
}

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);
  return <ProductView product={product} />;
}

// Переключить в полный SSR: каждый запрос рендерится заново.
// export const dynamic = "force-dynamic";

Один файл выражает все три режима: пути из generateStaticParams — это SSG, экспорт revalidate превращает кеш в ISR, а force-dynamic (или чтение cookies()/headers()) переводит маршрут в по-запросный SSR. Опасность в том, что всё это неявно: один вызов cookies() глубоко в общем компоненте молча превращает статический маршрут в SSR — и ваша «статическая» страница начинает долбить origin.

Почему это работает

Почему ISR отдаёт устаревшую страницу, а не заставляет посетителя ждать свежую? Потому что альтернатива привязывает ваш TTFB к самой медленной зависимости ровно тогда, когда это больнее всего. Если бы регенерация была блокирующей, первый посетитель после каждого истечения окна платил бы полную цену рендера — а под нагрузкой в этот зазор попадало бы много посетителей, и все бы ждали (или все запускали бы рендеры). Stale-while-revalidate разводит пути: чтение остаётся «отдать файл», запись происходит один раз, асинхронно, вне критического пути пользователя. Цена — честность про устарелость: при revalidate = 60 контракт звучит как «пользователи могут видеть данные возрастом до 60 секунд плюс время регенерации», и этот контракт действует на каждый путь и на каждый узел кеша, а не глобально.

Викторина

У ISR-страницы revalidate = 300. Контент в CMS меняется в 12:00. Посетитель открывает страницу в 12:04 (последний рендер был в 11:58). Что он видит и что происходит?

Матрица компромиссов: TTFB, свежесть, стоимость инфраструктуры

Разложите три стратегии по трём осям — и выбор станет механическим. TTFB: SSG выигрывает безоговорочно — файл с edge-узла CDN, 10–50 мс, origin не участвует. ISR на горячем пути такой же (это и есть закешированный файл) с редкой фоновой работой. SSR — единственный, у кого TTFB включает вычисления и загрузку данных: реалистично 100–500 мс из региона origin, хуже через континент, и деградирует под нагрузкой, потому что каждый запрос ест CPU сервера. Свежесть: SSR всегда актуален. ISR — ограниченно устаревший: максимум revalidate секунд плюс лаг регенерации. SSG заморожен до следующего деплоя; опечатка на полностью статической странице стоит полного цикла сборки и деплоя — на большом сайте это 10–20 минут. Стоимость инфраструктуры: SSG почти бесплатен на любом масштабе — 10 запросов или 10 миллионов, origin делает одинаково: ничего. ISR стоит один рендер на путь за окно, независимо от трафика — красивое свойство: миллион просмотров в час страницы с revalidate: 60 — это всё ещё ~60 рендеров в час. Стоимость SSR растёт линейно с трафиком; это единственная стратегия, где всплеск трафика — это всплеск вычислений, и единственная, где нужно думать про автоскейлинг и пулы соединений, чтобы пережить главную страницу Hacker News.

Честное правило выбора — не «что лучше», а «какую устарелость этот маршрут может себе позволить»: документация и маркетинг → SSG; листинги товаров, новости, общие для всех дашборды → ISR с окном под реальную частоту изменения данных; всё персонализированное, постраничное по пользователю или читающее cookies → SSR (точнее, динамический рендеринг с кешированными данными под капотом).

Выбери лучший вариант

Страница листинга товаров Next.js показывает цены и остатки, меняющиеся раз в несколько минут. Команде нужен TTFB менее 100 мс и допустима устарелость до 2 минут. Выберите стратегию рендеринга.

Сбои: stale после деплоя и лавина ревалидаций

Две продакшен-аварии определяют эту тему. Первая: ISR отдаёт устаревшее после деплоя. Кеш ISR привязан к сборке. При self-hosted развёртывании кеш по умолчанию живёт в файловой системе каждого инстанса — новый деплой стартует с пустым кешем (или хуже: несколько инстансов держат разные кеши, и пользователи видят разные версии в зависимости от того, в какой под их направил балансировщик). На Vercel кеш общий и переживает деплои для неизменённых страниц, но изменённая страница сбрасывается, и таймеры revalidate сбрасываются вместе с ней. В обоих случаях ментальная модель «деплой сбрасывает всё в свежее» неверна в обе стороны: часть страниц остаётся устаревшей, часть теряет кеш целиком. Для multi-instance self-hosted лекарство — общий обработчик кеша (cacheHandler в next.config.js, указывающий на Redis или аналог), чтобы все поды были согласны, что закешировано. Вторая: лавина ревалидаций (thundering herd). Когда запись популярной страницы истекает — или деплой опустошил кеш — каждый конкурентный запрос в этом зазоре может стать рендером. Внутри одного инстанса Next.js параллельные регенерации дедуплицируются; между инстансами — нет, если ваш cache handler не реализует блокировку. Десять подов × одна истёкшая горячая страница × CMS с rate limit = пятисотки из Hook. Смягчение: разносите окна, чтобы тысячи страниц не истекали синхронно; для данных, меняющихся по событию, предпочитайте on-demand ревалидацию (revalidatePath из вебхука), чтобы окна были длинными; и прогревайте критические пути сразу после деплоя, не отдавая это живым пользователям.

Викторина

Self-hosted Next.js работает в 8 подах за балансировщиком. Товарные страницы на ISR с revalidate = 60. Пользователи жалуются, что цены при обновлении страницы прыгают туда-сюда между старыми и новыми. Наиболее вероятная причина?

Вспомните перед уходом
  1. 01
    Опишите, что происходит, когда посетитель попадает на ISR-страницу с истёкшим окном revalidate. Кто и когда увидит свежий контент?
  2. 02
    Сравните SSG, SSR и ISR по TTFB, свежести и стоимости инфраструктуры и сформулируйте правило выбора.
Итог

Каждая стратегия отвечает на один вопрос: когда сервер превращает React-дерево в HTML? SSG отвечает «при сборке» — next build выдаёт статические файлы, CDN отдаёт их за 10–50 мс, origin не делает ничего на запрос, контент заморожен до следующего деплоя. SSR отвечает «на каждый запрос» — свежие данные и доступ к cookies и заголовкам, но TTFB теперь содержит слой данных (реалистично 100–500 мс), а вычислительная стоимость растёт линейно с трафиком. ISR — это stale-while-revalidate для целых страниц: отдать закешированный файл немедленно, даже устаревший; если запись старше окна revalidate — перерендерить в фоне, чтобы свежую копию получил следующий посетитель. Его цена — один рендер на путь за окно независимо от трафика, а контракт — ограниченная устарелость: окно плюс лаг регенерации, на каждый путь и каждый узел кеша. В App Router режим неявный: generateStaticParams пререндерит пути, экспорт revalidate превращает кеш в ISR, а чтение cookies() или force-dynamic переводит маршрут в SSR — один случайный вызов cookies() молча конвертирует статический маршрут в по-запросный рендеринг. Две продакшен-аварии, которые надо держать в голове: ISR-после-деплоя, когда self-hosted multi-instance развёртывания держат расходящиеся файловые кеши по подам (версии прыгают при обновлении — лечится общим cacheHandler), а деплой опустошает кеши, и первые запросы платят полные рендеры; и лавина ревалидаций, когда истечение горячей страницы на многих инстансах устраивает stampede по источнику данных — смягчается разнесением окон, on-demand revalidatePath по вебхукам для событийных данных и прогревом критических путей после деплоя. Теперь, когда страница ведёт себя странно после деплоя — показывает разные версии разным пользователям или падает с 500 под нагрузкой, — первый вопрос: в каком режиме рендеринга этот маршрут и кто опустошает его кеш?

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.