Розробка ISR (Incremental Static Regeneration) для веб-сайту
Incremental Static Regeneration — це SSG з можливістю оновлення окремих сторінок без повної пересборки сайту. Сторінка генерується один раз, кешується, а при застарінні кешу регенерується у фоні при наступному запиті. Користувач завжди отримує мигом відповідь з кешу, а свіжість контенту гарантується TTL.
ISR займає нішу між SSG (миттєва видача, застарілий контент) та SSR (свіжий контент, затримка сервера при кожному запиті).
Як працює ISR
Класична модель (Stale-While-Revalidate на рівні сторінок):
- Перший запит до сторінки — рендер на сервері, кешування HTML
- Повторні запити протягом TTL — видача з кешу, відповідь менше 10ms
- Запит після закінчення TTL — видача застарілого кешу (користувач не чекає), запуск фонової регенерації
- Наступний запит — свіжий HTML з оновленого кешу
Результат: TTFB як у статики, свіжість контенту як у SSR.
Реалізація в Next.js App Router
// app/products/[id]/page.tsx
interface Props {
params: { id: string };
}
async function getProduct(id: string): Promise<Product | null> {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: {
revalidate: 300, // Регенерація не частіше раз на 5 хвилин
tags: [`product-${id}`], // Тег для on-demand revalidation
},
});
if (!res.ok) return null;
return res.json();
}
export default async function ProductPage({ params }: Props) {
const product = await getProduct(params.id);
if (!product) notFound();
return <ProductView product={product} />;
}
// Попередня генерація популярних сторінок при збірці
export async function generateStaticParams() {
const popularProducts = await fetch('https://api.example.com/products?popular=true&limit=100')
.then(r => r.json());
return popularProducts.map(({ id }: { id: string }) => ({ id }));
}
Сторінки з generateStaticParams генерируються при збірці. Всі інші — при першому запиті (on-demand) і потім регенерируються за TTL.
On-Demand Revalidation — миттєве оновлення
TTL-кеш не підходить, коли потрібно оновити сторінку відразу після змін у CMS. Для цього — on-demand revalidation через API:
// app/api/revalidate/route.ts
import { revalidateTag, revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-revalidate-secret');
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { tag, path } = await request.json();
if (tag) {
revalidateTag(tag); // Інвалідувати всё з цим тегом
}
if (path) {
revalidatePath(path); // Інвалідувати конкретний шлях
}
return NextResponse.json({ revalidated: true, at: new Date().toISOString() });
}
Вебгачок з CMS викликає цей endpoint при публікації:
# Приклад запиту з Contentful вебгачка
curl -X POST https://example.com/api/revalidate \
-H "x-revalidate-secret: ${REVALIDATE_SECRET}" \
-H "Content-Type: application/json" \
-d '{"tag": "product-42"}'
Реалізація в Nuxt 3
<!-- pages/products/[id].vue -->
<script setup lang="ts">
const route = useRoute();
// Nuxt cachedFetch з TTL
const { data: product } = await useFetch<Product>(
`/api/products/${route.params.id}`,
{
key: `product-${route.params.id}`,
getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key],
}
);
// server/api/products/[id].ts використовує cache() з nitro
</script>
// server/api/products/[id].ts
import { defineEventHandler, getRouterParam } from 'h3';
export default cachedEventHandler(
async (event) => {
const id = getRouterParam(event, 'id');
return await $fetch(`https://api.example.com/products/${id}`);
},
{
maxAge: 300,
staleMaxAge: 3600,
name: 'product',
getKey: (event) => `product-${getRouterParam(event, 'id')}`,
}
);
Стратегії кешування
ISR дозволяє встановлювати різні TTL для різних типів сторінок:
| Тип сторінки | TTL | Логіка |
|---|---|---|
| Головна сторінка | 60 сек | Часто оновлюється |
| Сторінка категорії | 300 сек | Змінюється при додаванні товарів |
| Сторінка товара | 3600 сек | Стабільні дані, ціна — окремий запит |
| Стаття блогу | 86400 сек | Рідко редагується |
| Документація | Тільки on-demand | Тільки при публікації |
Кеш-сховище для розподіленого розгортання
Vercel використовує edge-кеш вбудовано. Для self-hosted Next.js потрібен зовнішній кеш:
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
cacheHandler: process.env.NODE_ENV === 'production'
? require.resolve('./cache-handler.js')
: undefined,
cacheMaxMemorySize: 0, // Вимкнути in-memory кеш з зовнішнім handler
};
// cache-handler.js — Redis-бекенд
const redis = require('ioredis');
const client = new redis(process.env.REDIS_URL);
module.exports = class CacheHandler {
async get(key) {
const data = await client.get(key);
return data ? JSON.parse(data) : null;
}
async set(key, data, ctx) {
const ttl = ctx.revalidate || 3600;
await client.setex(key, ttl, JSON.stringify({ value: data, lastModified: Date.now() }));
}
async revalidateTag(tag) {
// Сканування та видалення ключів з тегом
const keys = await client.smembers(`tag:${tag}`);
if (keys.length) await client.del(...keys);
await client.del(`tag:${tag}`);
}
};
Fallback-стратегії для нових сторінок
Для маршрутів, не згенерованих при збірці, три варіанти поведінки:
// Next.js: dynamicParams контролює поведінку
export const dynamicParams = true; // Генерувати on-demand (за замовчуванням)
// export const dynamicParams = false; // 404 для несгенерованих шляхів
// Nuxt: routeRules у nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/products/**': { isr: 300 }, // ISR з TTL 5 хвилин
'/blog/**': { isr: true }, // ISR тільки on-demand
'/dashboard/**': { ssr: true }, // Чистий SSR без кешу
'/static/**': { prerender: true }, // Тільки SSG
},
});
Моніторинг та відладка
Відстежуйте у production:
- Cache HIT rate — відношення відповідей з кешу до регенерацій
- Revalidation duration — час фонової регенерації (не повинен перевищувати TTL)
- Stale responses — кількість відповідей із застарілим контентом
// middleware.ts — логування статусу кешу
import { NextResponse, type NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set('x-cache-time', new Date().toISOString());
return response;
}
Строки реалізації
- Тиждень 1–2: вибір стеку, базова SSG/SSR структура, TTL-стратегії за типами сторінок
- Тиждень 3: on-demand revalidation, інтеграція вебгачків з CMS
- Тиждень 4: Redis cache handler для self-hosted, моніторинг cache hit rate
- Тиждень 5: навантажувальне тестування, оптимізація fallback-сторінок, документація для контент-команди
- Тиждень 6: розгортання, налаштування CI/CD з прогріванням кешу після збірки







