9. ISR — Инкрементальная регенерация
🔄 ISR — Инкрементальная статическая регенерация
Заголовок раздела «🔄 ISR — Инкрементальная статическая регенерация»ISR (Incremental Static Regeneration) — это магический компромисс между SSG и SSR. Представь: у тебя есть статичный ресторанный меню-сайт. Меню меняется раз в неделю. SSG слишком жёсткий (нужен редеплой при каждом изменении), SSR слишком дорогой (запрос к БД при каждом посещении). ISR — идеальный баланс! 🍕
SSG: [build] → [HTML] → пользователи (всегда одинаковый)SSR: [запрос] → [рендер] → [HTML] → пользователь (каждый раз новый рендер)ISR: [build] → [HTML] → пользователи (быстро как SSG!) ↕ каждые N секунд [регенерация] → [новый HTML] → следующий пользователь⏰ Временная ревалидация: revalidate
Заголовок раздела «⏰ Временная ревалидация: revalidate»Самый простой способ ISR — экспортировать revalidate:
// Обновлять страницу не чаще раз в 1 часexport const revalidate = 3600;
export default async function BlogPage() { const posts = await fetch('https://api.blog.com/posts', { next: { revalidate: 3600 }, // То же самое на уровне fetch }).then(r => r.json());
return ( <main> <h1>Блог</h1> <p>Обновляется каждый час</p> {posts.map(post => <PostCard key={post.id} post={post} />)} </main> );}// На уровне отдельного fetch (более гибко):export default async function ShopPage() { // Товары обновляются раз в час const products = await fetch('/api/products', { next: { revalidate: 3600 }, }).then(r => r.json());
// Цены обновляются каждые 5 минут const prices = await fetch('/api/prices', { next: { revalidate: 300 }, }).then(r => r.json());
// Баннер никогда не обновляется (статика) const banner = await fetch('/api/banner', { cache: 'force-cache', }).then(r => r.json());
return <ShopLayout products={products} prices={prices} banner={banner} />;}🏷️ Теговая ревалидация: On-demand ISR
Заголовок раздела «🏷️ Теговая ревалидация: On-demand ISR»Вместо ожидания таймера — сбрасываем кеш когда данные изменились:
// 1. Помечаем fetch тегамиexport default async function ProductPage({ params }: Props) { const { id } = await params;
const product = await fetch(`https://api.shop.com/products/${id}`, { next: { tags: ['products', `product-${id}`], // Теги для этого запроса }, }).then(r => r.json());
return <ProductDetail product={product} />;}
// app/products/page.tsxexport default async function ProductsPage() { const products = await fetch('https://api.shop.com/products', { next: { tags: ['products'] }, // Тот же тег 'products' }).then(r => r.json());
return <ProductsList products={products} />;}// 2. Создаём API для сброса кешаimport { revalidateTag, revalidatePath } from 'next/cache';import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) { const { searchParams } = new URL(request.url); const secret = searchParams.get('secret');
// Проверяем секрет (защита от несанкционированных сбросов) if (secret !== process.env.REVALIDATION_SECRET) { return NextResponse.json( { error: 'Invalid secret' }, { status: 401 } ); }
const body = await request.json();
if (body.tag) { revalidateTag(body.tag); // Сбрасываем по тегу return NextResponse.json({ revalidated: true, type: 'tag', tag: body.tag }); }
if (body.path) { revalidatePath(body.path); // Сбрасываем по пути return NextResponse.json({ revalidated: true, type: 'path', path: body.path }); }
return NextResponse.json({ error: 'No tag or path provided' }, { status: 400 });}// 3. Вызываем из CMS/webhook// Например, Sanity CMS webhook при обновлении продукта:// POST /api/revalidate?secret=MY_SECRET// Body: { "tag": "products" }
// Или для конкретного продукта:// Body: { "tag": "product-123" }
// Пример из Server Action:'use server';import { revalidateTag } from 'next/cache';
export async function updateProduct(id: string, data: ProductData) { await db.product.update({ where: { id }, data });
revalidateTag('products'); // Сбросить список всех продуктов revalidateTag(`product-${id}`); // Сбросить конкретный продукт}🗂️ revalidatePath: Сброс по маршруту
Заголовок раздела «🗂️ revalidatePath: Сброс по маршруту»import { revalidatePath } from 'next/cache';
// Сбросить конкретную страницуrevalidatePath('/blog'); // Только /blogrevalidatePath('/blog/hello-world'); // Только /blog/hello-world
// Сбросить все страницы по маршрутуrevalidatePath('/blog/[slug]', 'page'); // Все страницы блогаrevalidatePath('/blog', 'layout'); // Layout + все дочерние страницы
// Использование в Server Action'use server';export async function publishPost(id: string) { await db.post.update({ where: { id }, data: { published: true, publishedAt: new Date() } });
const post = await db.post.findUnique({ where: { id } });
revalidatePath('/blog'); // Обновить список постов revalidatePath(`/blog/${post?.slug}`); // Обновить страницу поста revalidatePath('/', 'layout'); // Обновить главную (если там список постов)}🧮 unstable_cache для не-fetch данных
Заголовок раздела «🧮 unstable_cache для не-fetch данных»Когда используешь ORM напрямую — нет встроенного кеша. unstable_cache спасает:
import { unstable_cache } from 'next/cache';import { db } from '@/lib/prisma';
// Создаём кешированную функциюconst getCachedProducts = unstable_cache( async (category: string) => { console.log('DB query!'); // Выполнится только при кеш-мисс return await db.product.findMany({ where: { category, published: true }, include: { images: true }, orderBy: { createdAt: 'desc' }, }); }, ['products-by-category'], // Ключ кеша (уникальный идентификатор) { revalidate: 300, // Обновлять каждые 5 минут tags: ['products'], // Теги для on-demand сброса });
// Использованиеexport default async function CategoryPage({ params }: Props) { const { category } = await params;
// Первый вызов: запрос к БД // Последующие: из кеша (до истечения revalidate) const products = await getCachedProducts(category);
return <ProductGrid products={products} />;}🌊 Stale-While-Revalidate: Механизм ISR
Заголовок раздела «🌊 Stale-While-Revalidate: Механизм ISR»Понимание механизма поможет избежать сюрпризов:
Запрос 1 (t=0s): кеш пуст→ Рендерим страницу (SSG/ISR)→ Сохраняем в кеш→ Отвечаем пользователю: свежие данные ✅
Запрос 2 (t=30min): кеш актуален (revalidate=3600)→ Отдаём из кеша: быстро ⚡
Запрос 3 (t=2h): кеш устарел (>3600s)→ Отдаём УСТАРЕВШИЕ данные (stale): быстро ⚡→ В фоне: запускаем регенерацию страницы→ Кеш обновляется
Запрос 4 (после регенерации):→ Отдаём из кеша: быстро ⚡ + свежие данные ✅// ВАЖНО: ISR не гарантирует мгновенное обновление!// Первый запрос ПОСЛЕ истечения revalidate получит старые данные// + запустит регенерацию в фоне// Следующий запрос получит свежие данные
// Если нужно мгновенное обновление — используй on-demand revalidation:'use server';export async function updateBlogPost(data: UpdatePostData) { await db.post.update({ where: { id: data.id }, data });
revalidateTag('posts'); // Мгновенный сброс кеша! revalidatePath(`/blog/${data.slug}`);}📊 Сравнение ISR подходов
Заголовок раздела «📊 Сравнение ISR подходов»| Временная ревалидация | Теговая ревалидация | |
|---|---|---|
| Как | revalidate = N | revalidateTag() / revalidatePath() |
| Когда | Автоматически по таймеру | Вручную (при изменении данных) |
| Гарантия свежести | Максимум N секунд | Почти мгновенно |
| Сложность | Простая | Требует webhook/action |
| Лучше для | Данные меняются регулярно | Данные меняются редко/непредсказуемо |
🎯 Практические паттерны
Заголовок раздела «🎯 Практические паттерны»// Паттерн 1: Разные revalidate для разных частейexport default async function ShopPage() { const [categories, featuredProducts, banners] = await Promise.all([ // Категории — редко меняются fetch('/api/categories', { next: { revalidate: 86400 } }).then(r => r.json()), // Рекомендованные — раз в час fetch('/api/featured', { next: { revalidate: 3600 } }).then(r => r.json()), // Баннеры — всегда свежие (CMS driven) fetch('/api/banners', { next: { tags: ['banners'] } }).then(r => r.json()), ]);
return <ShopLayout categories={categories} featured={featuredProducts} banners={banners} />;}
// Паттерн 2: ISR + Suspense для долгих регенераций// app/analytics/page.tsxexport const revalidate = 60;
export default function AnalyticsPage() { return ( <div> <h1>Аналитика</h1> <Suspense fallback={<ChartSkeleton />}> <SlowAnalyticsChart /> {/* Может быть медленным при регенерации */} </Suspense> </div> );}🎯 Резюме урока
Заголовок раздела «🎯 Резюме урока»- ISR = SSG + автоматическое обновление
revalidate = N— обновлять каждые N секундrevalidateTag(tag)— мгновенный сброс кеша по тегуrevalidatePath(path)— сброс по маршруту- Stale-while-revalidate — пользователь получает быстрый ответ, обновление в фоне
- unstable_cache — кеш для ORM/не-fetch данных