Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює інкрементальна статична регенерація (ISR) у Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**ISR (Інкрементальна статична регенерація)** - стратегія Next.js, яка попередньо рендерить сторінки при збірці та оновлює їх у фоні після спливання таймера `revalidate`, без перезбірки всього застосунку. ```tsx // App Router: кеш на 5 хвилин, регенерація при наступному запиті після спливання const res = await fetch('https://api.example.com/data', { next: { revalidate: 300 } }); ``` **Ключове:** перший запит після спливання таймера отримує застарілу версію, наступний - вже свіжу. Якщо `getStaticProps` падає під час фонової регенерації, застаріла сторінка продовжує віддаватися.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**ISR (Інкрементальна статична регенерація)** - стратегія Next.js, яка будує сторінки статично при деплої, а потім оновлює окремі сторінки у фоні після спливання таймера `revalidate`, без перезбірки всього застосунку. ## Теорія ### TL;DR - Аналогія: кав'ярня з готовими чайниками. Чайник стоїть на прилавку (статика), але коли він спустошується, машина автоматично заварює новий. Ти берешь те, що є; наступний клієнт отримує свіже. - Головна різниця: SSG потребує повної `next build` щоб оновити вміст. ISR оновлює окремі сторінки за таймером без перезбірки. SSR рендерить на кожен запит; ISR віддає з кешу. - Правило вибору: дані змінюються кожні кілька хвилин або годин і більшість запитів йде на одні й ті ж сторінки - ISR підходить. Дані персональні - SSR. Ніколи не змінюються - чистий SSG. - Перший запит після спливання таймера отримує застарілу версію. Наступний - вже свіжу. - Реvalідація за подією (`revalidatePath`, `revalidateTag`) точніша за таймер: оновлюєш кеш саме тоді, коли дані змінились. ### Швидкий приклад ```tsx // pages/posts.js - базовий ISR, Next.js 12+ export async function getStaticProps() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()); return { props: { posts }, revalidate: 60 // регенерувати не частіше одного разу за 60 секунд }; } export default function Posts({ posts }) { return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>; } // t=0: статичний HTML на CDN. // t=61с, запит 1: отримує застарілу HTML, getStaticProps стартує у фоні. // t=61с, запит 2: отримує щойно згенеровану сторінку. ``` Поле `revalidate: 60` серіалізується у статичний JSON payload під час збірки. Next.js зчитує його при кожній перевірці кешу і вирішує, чи ставити регенерацію в чергу. ### Ключова різниця ISR - це SSG з вбудованим таймером спливання. При збірці `getStaticProps` відпрацьовує звично і Next.js записує `/_next/static/[page].json` та HTML на диск або CDN. Різниця в тому, що цей JSON також зберігає значення `revalidate`. Коли запит приходить після спливання таймера, Node.js ставить у чергу фоновий виклик `getStaticProps`, атомарно перезаписує HTML та JSON через `fs.promises`, і продовжує віддавати застарілу версію поки запис не завершиться. Без простою, без повної перезбірки, жоден користувач не чекає. ### Коли використовувати - Дані оновлюються раз на годину або раз на день (статті, каталоги продуктів, документація) - ISR замість SSG - Високий трафік, рідкі зміни - ISR замість SSR (немає витрат на рендер при кожному запиті) - Дані в реальному часі або прив'язані до конкретного користувача - SSR - Контент, який ніколи не змінюється - чистий SSG, ISR тут нічого не додає - Часті зміни плюс персоналізація - SSR або клієнтський фетч ### Порівняльна таблиця | | SSG | ISR | SSR | |---|---|---|---| | Генерація | При збірці | При збірці + фон | Кожен запит | | Перший завантаж | Миттєво (CDN) | Миттєво (CDN) | ~100-500мс на сервері | | Оновлення | Повна `next build` | Фоново за таймером | Кожен запит | | Застарілі відповіді | Ніколи | Так, до завершення регенерації | Ніколи | | Коли | Документація, маркетинг | Новини, каталоги, дашборди | Профілі, кошики | ### Як Next.js обробляє регенерацію всередині При збірці `getStaticProps` серіалізує пропси в `/_next/static/[page].json` з полем `revalidate` і розгортає HTML на CDN. Коли приходить запит після спливання таймера (перевіряється через заголовки Vercel Edge або мітку часу Node-кешу), Next.js виконує чотири кроки по порядку: 1. Відразу віддає застарілу сторінку поточному користувачу 2. Ставить `getStaticProps` у чергу воркер-потоку 3. Атомарно записує новий JSON та HTML на файлову систему або CDN 4. Всі паралельні запити під час регенерації також отримують застарілу версію Якщо `getStaticProps` падає під час фонової регенерації, Next.js логує помилку і продовжує віддавати останню вдалу версію. Сторінка залишається доступною. ### ISR у App Router В App Router параметр `revalidate` переходить безпосередньо у виклик `fetch`: ```tsx // app/problems/page.tsx export default async function ProblemsPage() { const res = await fetch('https://api.itlead.org/problems', { next: { revalidate: 300 } // вікно кешування 5 хвилин }); const problems = await res.json(); return ( <ul> {problems.map((p: { id: string; name: string }) => ( <li key={p.id}>{p.name}</li> ))} </ul> ); } ``` Або на рівні сегмента маршруту через експортовану константу: ```tsx // app/problems/layout.tsx export const revalidate = 300; export default function ProblemsLayout({ children }: { children: React.ReactNode }) { return <>{children}</>; } ``` Для оновлення за подією, а не за таймером, використовуй `revalidatePath` або `revalidateTag`. На практиці цей підхід зручніший: інвалідуєш кеш саме тоді, коли CMS надіслав webhook, а не чекаєш на фіксоване вікно. ```tsx // 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: 'Невірний секрет' }, { status: 401 }); } revalidatePath(path); return NextResponse.json({ revalidated: true }); } ``` Тег-based інвалідація дозволяє скидати кеш одразу для кількох сторінок: ```tsx // Тегуємо fetch const res = await fetch('https://api.itlead.org/problems', { next: { tags: ['problems'] } }); // Інвалідуємо всі сторінки, що використовували цей тег revalidateTag('problems'); ``` ### Типові помилки **`revalidate: 0` в розрахунку отримати поведінку SSR** ```tsx // Неправильно - це не getServerSideProps return { props: { data }, revalidate: 0 }; ``` `revalidate: 0` не дає рендеринг на кожен запит. Сторінка залишається статичною і намагається регенеруватися при кожному запиті, але все одно підпадає під кешування. Для справжнього per-request рендерингу використовуй `getServerSideProps`. **Зміна стану на рівні модуля всередині `getStaticProps`** ```tsx // Неправильно let cache: string[] = []; export async function getStaticProps() { cache.push(await fetchData()); // стан втрачається між викликами Lambda } ``` `getStaticProps` запускається в ізольованих Lambda або VM-контекстах. Змінні рівня модуля не зберігаються між викликами. Для спільного стану використовуй Redis, базу даних або `unstable_cache()` у Next.js 14+. **Ігнорування стрибків TTFB від `fallback: 'blocking'` під навантаженням** ```tsx export async function getStaticPaths() { return { paths: [{ params: { slug: 'post1' } }], fallback: 'blocking' }; } ``` З `fallback: 'blocking'` невідомі slug-и рендеряться на сервері при першому запиті, потім кешуються як ISR. Цей перший рендер блокуючий і послідовний. При раптових сплесках трафіку для нових slug-ів p99 TTFB помітно зростає. Попередньо збирай популярні шляхи або використовуй `fallback: true` зі станом завантаження. **`getStaticProps` що виконується довше 10 секунд** Vercel обриває виконання після 10 секунд як під час збірки, так і в продакшені. Якщо CMS повертає 500 елементів - пагінуй запити. Для великих сайтів розділи фетчі на менші шматки. **Preview mode без явного `revalidate: false`** Коли активний preview mode, таймер `revalidate` ігнорується, але чернетки можуть потрапити в CDN-кеш і з'явитися у реальних користувачів. Завжди повертай `revalidate: false` явно: ```tsx if (preview) return { props: { draftData }, revalidate: false }; ``` ### Де зустрічається в реальних проєктах - Блог Vercel: `revalidate: 60`, статті оновлюються без деплою - Документація Stripe: changelog продуктів через Git-based CMS, `revalidate: 3600` - Hashnode: стрічки блогів через ISR, нові сторінки авторів через `fallback: blocking` - Блог Лі Робінсона: список доповідей з Airtable, оновлюється через ISR - TinaCMS + Next.js starter: ISR у поєднанні з preview mode для headless CMS ### Питання на співбесіді **Q:** Як ISR обробляє паралельні запити під час регенерації? **A:** Всі запити під час регенерації отримують застарілу сторінку. Next.js використовує атомарний `fs.writeFile` та перевірку ETag щоб уникнути race conditions. Трафік перемикається на нову версію тільки після завершення запису. **Q:** Яка різниця між `revalidate: 60` у `getStaticProps` і `revalidatePath()`? **A:** `revalidate` - це тригер за часом: сторінка регенерується не частіше одного разу за N секунд після першого запиту після спливання. `revalidatePath('/path')` - це ручний виклик: ти запускаєш його з Route Handler або Server Action, і кеш скидається одразу незалежно від таймера. **Q:** Що станеться, якщо `getStaticProps` падає під час фонової регенерації? **A:** Next.js логує помилку і продовжує віддавати останню вдалу застарілу версію. Повернення до SSR не відбувається. Сторінка залишається доступною до першої успішної регенерації. **Q:** ISR однаково працює на self-hosted Node і на Vercel? **A:** Механізм той самий, але self-hosted потребує постійної файлової системи або кастомного cache handler для розподілених інстансів. Vercel автоматично масштабує воркери і ділить кеш між інстансами через Edge Network без додаткових налаштувань. **Q (senior):** В App Router (Next.js 14+), як `{ next: { revalidate: 60 } }` у fetch відрізняється від `revalidatePath`? Які шари кешу задіяні? **A:** `fetch` з `revalidate` впливає на Data Cache - серверний кеш відповідей окремих запитів. `revalidatePath` інвалідує Full Route Cache (відрендерений RSC payload та HTML) і очищає Router Cache на клієнті. Якщо викликати тільки `revalidatePath`, старі відповіді `fetch` залишаться у Data Cache до спливання їх власного таймера. Для повної свіжості зазвичай потрібно обидва: `revalidateTag` для Data Cache і `revalidatePath` для Route Cache. **Q (senior):** Як масштабувати ISR на 10 000 сторінок з різними інтервалами revalidate? **A:** Не збирай всі 10к при деплої. Використовуй динамічні сегменти з `fallback: 'blocking'`, щоб сторінки будувались при першому запиті і кешувались як ISR. Встановлюй коротший `revalidate` для high-traffic маршрутів і довший для хвоста. На self-hosted налаштуй Redis-based cache handler щоб уникнути конкуренції за файлову систему між кількома Node-інстансами. ## Приклади ### Базовий: список статей з таймерним ISR (Pages Router) ```tsx // pages/posts.tsx interface Post { id: number; title: string; } export async function getStaticProps() { const posts: Post[] = await fetch('https://api.example.com/posts') .then(r => r.json()); return { props: { posts }, revalidate: 60 }; } export default function Posts({ posts }: { posts: Post[] }) { return ( <ul> {posts.map(post => <li key={post.id}>{post.title}</li>)} </ul> ); } // t=0: статичний HTML на CDN. // t=61с запит 1: застаріла HTML, фонова регенерація стартує. // t=61с запит 2: щойно згенерована сторінка. ``` `revalidate: 60` у return - це весь контракт ISR з Next.js. Все інше - стандартний `getStaticProps`. ### Середній: каталог продуктів з Contentful з 5-хвилинною реvalідацією і безпечним preview mode ```tsx // pages/products.tsx import { createClient } from 'contentful'; interface Product { sys: { id: string }; fields: { name: string; price: number }; } export async function getStaticProps({ preview = false }: { preview?: boolean }) { const client = createClient({ space: process.env.CONTENTFUL_SPACE_ID!, accessToken: preview ? process.env.CONTENTFUL_PREVIEW_TOKEN! : process.env.CONTENTFUL_ACCESS_TOKEN!, }); const entries = await client.getEntries<any>({ content_type: 'product', limit: 20, }); // Preview mode: ніколи не кешуємо чернетки if (preview) { return { props: { products: entries.items }, revalidate: false }; } return { props: { products: entries.items }, revalidate: 300 // 5 хвилин покриває більшість циклів оновлення цін }; } export default function Products({ products }: { products: Product[] }) { return ( <div> {products.map(p => ( <div key={p.sys.id}> <h2>{p.fields.name}</h2> <p>${p.fields.price}</p> </div> ))} </div> ); } ``` Без `revalidate: false` у preview mode чернетки можуть потрапити в CDN-кеш і показатися реальним користувачам. Цей патерн легко пропустити, і він зустрічається у продакшн-інцидентах частіше, ніж здається. ### Просунутий: динамічні slug-и з `fallback: 'blocking'` та webhook-driven реvalідацією (App Router) ```tsx // app/articles/[slug]/page.tsx interface Article { slug: string; title: string; body: string; } // При деплої будуємо тільки топ-10; невідомі slug-и рендеряться при першому запиті export async function generateStaticParams() { const articles: Article[] = await fetch( 'https://api.example.com/articles?limit=10' ).then(r => r.json()); return articles.map(a => ({ slug: a.slug })); } export const dynamicParams = true; // дозволяємо невідомі slug-и export const revalidate = 10; // таймер як запасний варіант export default async function ArticlePage({ params }: { params: { slug: string } }) { const article: Article = await fetch( `https://api.example.com/articles/${params.slug}`, { next: { tags: [`article-${params.slug}`] } } // тег для точкової інвалідації ).then(r => r.json()); return ( <article> <h1>{article.title}</h1> <p>{article.body}</p> </article> ); } ``` ```tsx // app/api/revalidate/route.ts - викликається з CMS webhook при публікації import { revalidateTag } from 'next/cache'; import { NextResponse } from 'next/server'; export async function POST(request: Request) { const { secret, slug } = await request.json(); if (secret !== process.env.REVALIDATION_SECRET) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } revalidateTag(`article-${slug}`); return NextResponse.json({ revalidated: true, slug }); } ``` Перший запит для невідомого slug-а блокується (як SSR), а потім кешується як ISR-сторінка. Після цього CMS надсилає webhook при публікації і `revalidateTag` скидає кеш одразу. Таймер 10 секунд спрацьовує як запасний механізм якщо webhook не дійшов. Обидва підходи разом дають оновлення в реальному часі без відмови від статичної швидкості для відомих сторінок.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.