Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Стратегії повторної валідації в Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Revalidation** (повторна валідація) в Next.js оновлює кешовані сторінки без повного перебудови. ```tsx fetch(url, { next: { revalidate: 3600 } }); // Роздає кеш миттєво; фоновий fetch запускається після 1 години ``` **Ключове:** time-based (`revalidate: N`) для даних з регулярними змінами; on-demand (`revalidatePath`, `revalidateTag`) для миттєвої інвалідації після мутацій.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Revalidation** (повторна валідація) в Next.js оновлює кешовані сторінки свіжими даними без повного перебудови сайту. ## Теорія ### TL;DR - Аналогія: кав'ярня з готовою кавою в термосах. Клієнти завжди отримують каву з термоса (кеш), але щогодини хтось паралельно заварює нову порцію, поки стара ще роздається. - Time-based revalidation оновлює за розкладом (stale-while-revalidate); on-demand revalidation інвалідує кеш одразу при зміні даних. - Правило вибору: time-based для контенту, який рідко змінюється (блог, документація); on-demand там, де застарілі дані справді шкодять (ціни, залишки товарів). - `revalidateTag` дає точковий контроль: один тег може покривати кілька fetch-запитів на різних маршрутах. ### Швидкий приклад ```tsx // app/products/page.tsx (App Router) async function getProducts() { const res = await fetch('https://api.example.com/products', { next: { revalidate: 3600 } // Кеш на 1 годину, потім фоновий fetch }); return res.json(); } export default async function ProductsPage() { const products = await getProducts(); // Перший візит: завантажує живі дані, зберігає в кеш // Візити протягом 1 години: з кешу (~50ms) // Перший візит після 1 години: отримує кешовану версію, // але запускає фоновий fetch для наступного відвідувача return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>; } ``` Останній момент найчастіше пропускають. Користувач, який тригерує revalidation, все одно бачить застарілі дані. Свіжу версію отримає вже *наступний* відвідувач. ### Головна різниця Time-based і on-demand revalidation вирішують різні задачі. Time-based реалізує stale-while-revalidate: сторінка віддається з кешу миттєво, а Next.js запускає фоновий процес для регенерації після закінчення TTL. On-demand взагалі ігнорує таймер. Webhook або server action викликає `revalidatePath()` чи `revalidateTag()`, і кеш інвалідується одразу. Ніякого очікування. ### Коли використовувати - Блог, документація, SEO-сторінки: `revalidate: 86400` (щодня). Стаття добової давності - нормально. - Каталог е-комерсу: `revalidate: 300` (5 хвилин) як запасний варіант плюс `revalidateTag('products')` з Shopify/Stripe webhook для миттєвих оновлень. - Актуальні курси, біржові котирування, рахунки в грі: `cache: 'no-store'` або `export const dynamic = 'force-dynamic'`. Кешувати не потрібно взагалі. - Дані конкретного користувача (дашборди, профілі): `noStore()` або динамічний рендеринг на кожен запит. ISR тут не підходить. - Великі сайти з пов'язаним контентом (кошик + список товарів): tag-based revalidation з `revalidateTag('cart')`, щоб оновлювати тільки те, що змінилось. ### Таблиця порівняння | Стратегія | Тригер | Допустима застарілість | Де використовувати | Вплив на затримку | |---|---|---|---|---| | Time-based (ISR) | Таймер (`revalidate: 3600`) | До закінчення інтервалу | Блоги, SEO-сторінки | Немає (stale-while-revalidate) | | On-demand path | `revalidatePath('/products')` | Нуль після тригера | CMS, мутації | Немає (фоново) | | On-demand tag | `revalidateTag('products')` | Нуль після тригера | Е-комерс, точкові оновлення | Немає (фоново) | | Per-request dynamic | На кожен запит | Немає | Дашборди, дані юзера | Висока (завжди fetch) | | Без кешу | `cache: 'no-store'` | Немає | Real-time дані | Висока (завжди fetch) | | Коли використовувати | Рідкі зміни → time-based; терміново → on-demand; дані юзера → dynamic | | | | ### Як це працює всередині При першому запиті Next.js генерує сторінку і зберігає результат (HTML + JSON) в `.next/cache` на диску або в розподіленому кеші Vercel. Наступні запити перевіряють, чи не закінчився TTL. Якщо ні, кешована відповідь іде миттєво. Якщо TTL вийшов, застаріла відповідь все одно іде миттєво (це і є stale-while-revalidate), але Next.js запускає фоновий Node.js worker для повторного fetch і регенерації сторінки. Після завершення кеш атомарно замінюється, і наступний запит отримає свіжу версію. On-demand revalidation взагалі не дивиться на TTL. `revalidatePath` або `revalidateTag` позначають конкретні записи в кеші як застарілі прямо на сервері. Наступний запит до цього шляху запустить свіжий fetch, не плановий. Важливий edge case: Router Cache (кеш оболонок сторінок у браузері) і Data Cache (серверний) - це різні рівні. `revalidatePath('/products')` інвалідує Data Cache, але Router Cache в браузері може зберігатись ще до 30 секунд у Next.js 14. Якщо потрібно оновити і layout теж, використовуй `revalidatePath('/', 'layout')`. ### Типові помилки **1. Поєднання `revalidate` з динамічним рендерингом** ```tsx // Неправильно: searchParams вмикає динамічний рендеринг, revalidate ігнорується export const revalidate = 3600; export default function Page({ searchParams }: any) { // searchParams робить сторінку динамічною // revalidate тут нічого не робить const query = searchParams.q; return <Results query={query} />; } ``` В Next.js 14+ `searchParams` переключає сторінку на динамічний рендеринг. Експорт `revalidate` при цьому ігнорується. Потрібно або прибрати `revalidate` і прийняти динамічну поведінку, або явно додати `noStore()`. **2. Очікування фонового оновлення без трафіку** ```tsx fetch(url, { next: { revalidate: 3600 } }); // Якщо після закінчення TTL ніхто не зайде на сторінку, // кеш так і залишиться застарілим назавжди. // Фонового крону немає - revalidation потребує вхідного запиту. ``` На сторінках з низьким трафіком додай cron-джоб, який регулярно звертається до URL, або переходь на on-demand revalidation через webhook. **3. Плутанина між Router Cache і Data Cache** ```tsx // Після revalidatePath('/') Data Cache оновлюється. // Але Router Cache (оболонка сторінки в браузері) може ще бути застарілою. // Рішення: revalidatePath('/', 'layout'); // Проходить через всі сегменти layout ``` **4. `revalidateTag` на fetch без тегів** ```tsx // Цей fetch без тегу - revalidateTag його не зачепить fetch(url, { next: { revalidate: 60 } }); // Далі: revalidateTag('products'); // Нічого не робить для fetch вище ``` Завжди додавай `{ tags: ['products'] }` до fetch-запитів, якими хочеш керувати через `revalidateTag`. Time-based і tag-based опції для одного fetch є взаємовиключними. ### Де зустрічається в реальних проєктах - **Vercel Commerce (Next.js Commerce):** on-demand webhook від Shopify/Stripe тригерить `revalidateTag('products')` при зміні товарів. - **TinaCMS:** time-based ISR з `revalidate: 10` секунд для markdown-контенту, зміни в редакторі з'являються практично одразу без перебудови. - **Supabase + Next.js:** `` revalidateTag(`user-${id}`) `` після зміни стану авторизації. - **Medusa.js:** гібридний підхід - time-based для каталогу, on-demand для статусів замовлень. - Для порівняння: revalidation замість `dynamic` рендерингу дає перевагу там, де трафік великий і дані напівстатичні. Різниця між кешованою відповіддю (до 50ms) і динамічним fetch (200-500ms+) відчутна при навантаженні. ### Питання на співбесіді **Q:** Яка різниця між stale-while-revalidate в Next.js і директивою `stale-while-revalidate` в HTTP Cache-Control? **A:** В Next.js це серверна поведінка: Next.js роздає кешовану сторінку і паралельно регенерує її через Node.js worker. HTTP-директива робить те саме, але на рівні CDN чи браузера (наприклад, Cloudflare). Вони можуть працювати разом: Next.js оновлює серверний кеш, CDN тим часом роздає свою застарілу копію. **Q:** Чим відрізняється revalidation в App Router від Pages Router? **A:** Pages Router має один ISR-запис на шлях без підтримки тегів. App Router додає окремі рівні Data Cache і Router Cache, підтримує `revalidateTag` для точкової інвалідації між кількома маршрутами і дозволяє revalidation на рівні сегментів layout. **Q:** Що відбувається при виклику `revalidatePath` під час Server Action? **A:** Next.js позначає кеш-запис шляху як застарілий одразу. Але сама revalidation відбувається після завершення Server Action, не під час. Спочатку іде відповідь, потім чиститься кеш. **Q:** (Senior) У вкладеному layout зі спільними даними між сегментами - як оновити лічильник кошика в хедері без перефетчу всього? **A:** Позначити fetch в layout-компоненті тегом `{ tags: ['cart'] }`. Після мутації кошика в Server Action або webhook викликати `revalidateTag('cart')`. Тільки теговані fetches регенеруються, решта layout залишається в кеші. Це краще ніж `revalidatePath('/', 'layout')`, який інвалідує все підряд. ## Приклади ### Базовий: time-based revalidation для блогу ```tsx // app/blog/page.tsx async function getPosts() { const res = await fetch('https://cms.example.com/posts', { next: { revalidate: 86400 } // Оновлення раз на день }); return res.json(); } export default async function BlogPage() { const posts = await getPosts(); return ( <ul> {posts.map((post: { id: string; title: string }) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); } ``` Стаття добової давності цілком нормально. Сторінка завантажується за 50ms з кешу, читачі не помічають фонової регенерації. ### Середній: on-demand revalidation через Stripe webhook ```tsx // app/shop/products/page.tsx async function getProducts() { const res = await fetch('https://api.stripe.com/v1/products?limit=10', { next: { tags: ['featured-products'] } // Без таймера - оновлюється тільки коли прийде webhook }); return res.json(); } export default async function ProductsPage() { const { data: products } = await getProducts(); return products.map((p: { id: string; name: string }) => ( <div key={p.id}>{p.name}</div> )); } ``` ```tsx // app/api/revalidate/route.ts import { revalidateTag } from 'next/cache'; export async function POST(request: Request) { // В продакшені: тут перевіряти підпис Stripe webhook revalidateTag('featured-products'); return Response.json({ revalidated: true, timestamp: Date.now() }); } ``` Коли Stripe надсилає подію `product.updated`, webhook вдаряє по `/api/revalidate`, тег інвалідується, і наступний відвідувач `/shop/products` отримує свіжі дані. Всі до цього моменту бачать кеш. Це очікувана поведінка. ### Просунутий: Server Action з tag-based revalidation ```tsx // actions/posts.ts 'use server'; import { revalidateTag } from 'next/cache'; export async function createPost(formData: FormData) { const title = formData.get('title') as string; await db.insert(posts).values({ title }); // Інвалідує всі fetches з тегом 'posts' на всіх маршрутах revalidateTag('posts'); } ``` ```tsx // app/blog/new/page.tsx 'use client'; import { createPost } from '@/actions/posts'; export function NewPostForm() { return ( <form action={createPost}> <input name="title" placeholder="Назва поста" required /> <button type="submit">Опублікувати</button> </form> ); } ``` Після відправки форми `revalidateTag('posts')` спрацьовує, і всі маршрути з `{ tags: ['posts'] }` отримають свіжі дані при наступному запиті. Окремий API route не потрібен: Server Actions обробляють revalidation напряму.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.