Как работает Incremental Static Regeneration (ISR) в Next.js
ISR (Incremental Static Regeneration) это стратегия, которая объединяет преимущества SSG и SSR. Страница генерируется статически, но может обновляться в фоне через заданный интервал без полной пересборки приложения.
Проблема, которую решает ISR
Представь, что на Hack Frontend есть каталог задач. Он обновляется пару раз в день. SSG не подходит, потому что после добавления новой задачи нужно делать полный next build. SSR не подходит, потому что каталог одинаков для всех пользователей и нет смысла генерировать его при каждом запросе.
ISR решает эту дилемму: страница статическая, но умеет обновляться.
Как это работает
Первая сборка
Страница генерируется статически, как при SSG. HTML кешируется.
Запрос в пределах revalidate
Пользователь получает кешированную версию страницы. Никакого рендеринга на сервере.
Запрос после истечения revalidate
Пользователь все еще получает кешированную версию (stale), но Next.js запускает регенерацию в фоне. Следующий пользователь получит уже обновленную страницу.
Этот подход называется stale-while-revalidate: отдаем устаревшее, пока готовим свежее.
ISR в App Router
В App Router ISR настраивается через параметр revalidate в fetch:
// app/problems/page.tsx
export default async function ProblemsPage() {
const res = await fetch('https://api.hackfrontend.com/problems', {
next: { revalidate: 300 }
})
const problems = await res.json()
return (
<div>
<h1>Задачи на Hack Frontend</h1>
<ul>
{problems.map((p: { id: string; name: string }) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</div>
)
}
revalidate: 300 значит: кешировать страницу на 5 минут, после чего обновить в фоне.
Настройка на уровне сегмента
Можно задать revalidate для всего роута:
// app/problems/layout.tsx
export const revalidate = 300
export default function ProblemsLayout({
children
}: {
children: React.ReactNode
}) {
return <>{children}</>
}
On-Demand Revalidation
Вместо интервала можно обновлять страницу по событию. Например, после добавления новой задачи:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const { secret, path } = await request.json()
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 })
}
revalidatePath(path)
return NextResponse.json({ revalidated: true })
}
Или с использованием тегов:
// Загрузка данных с тегом
const res = await fetch('https://api.hackfrontend.com/problems', {
next: { tags: ['problems'] }
})
// Инвалидация по тегу после обновления данных
revalidateTag('problems')
На практике:
On-demand revalidation удобнее, чем интервальный подход. Ты обновляешь страницу именно тогда, когда данные изменились, а не через фиксированный промежуток.
ISR в Pages Router (Legacy)
В Pages Router ISR включается добавлением revalidate в getStaticProps:
// pages/problems.tsx
export async function getStaticProps() {
const res = await fetch('https://api.hackfrontend.com/problems')
const problems = await res.json()
return {
props: { problems },
revalidate: 300
}
}
Когда использовать ISR
Подходит:
- Каталоги и списки (задачи, товары, курсы)
- Контент, обновляемый несколько раз в день
- Страницы, где допустима задержка в обновлении на несколько минут
- Большие сайты, где полная пересборка занимает слишком много времени
Не подходит:
- Персонализированный контент (нужен SSR)
- Данные в реальном времени (нужен SSR или CSR с WebSocket)
- Контент, который никогда не меняется (достаточно SSG)
Сравнение стратегий
| SSG | ISR | SSR | |
|---|---|---|---|
| Генерация | При сборке | При сборке + фоновое обновление | При каждом запросе |
| Актуальность | На момент сборки | С задержкой (revalidate) | Всегда актуальные |
| Скорость | Мгновенная | Мгновенная | Зависит от сервера |
| Пересборка | Полная | Не нужна | Не нужна |
Полезные ресурсы
- nextjs.org/docs — ревалидация данных
- On-Demand Revalidation — инвалидация по событию