Next.js 16 revalidateTag та revalidatePath: повний гід із інвалідації кешу (2026)
Практичний гід із revalidateTag та revalidatePath у Next.js 16: cacheTag, вебхуки CMS, self-hosted-налаштування з Redis, часті помилки та їх діагностика.
revalidateTag та revalidatePath є двома основними API інвалідації кешу в Next.js 16: перше очищає всі закешовані значення, позначені конкретним тегом, а друге скидає повний кеш для маршруту (або кількох, якщо задано динамічний сегмент). Правило простоти таке: використовуйте revalidateTag для точкової інвалідації даних (запис у базу, вебхук від CMS), а revalidatePath, коли змінюється сам маршрут (нова сторінка, зміна URL-структури). Чесно кажучи, я тримаю Next.js у проді ще з часів pages-router, і після переходу на App Router ці два виклики стали найчастішими в моїх Server Actions.
revalidateTag(tag) інвалідує всі функції з cacheTag(tag) або fetch-запити з next: { tags: [tag] }. Це найточніший інструмент.
revalidatePath(path, type?) очищає весь кеш маршруту; передайте 'layout' або 'page', щоб керувати глибиною скидання.
Обидва API виключно серверні: викликайте їх усередині Server Actions або Route Handlers, ніколи в компоненті сторінки.
З директивою 'use cache' у Next.js 16 функції cacheTag() та cacheLife() замінюють старий синтаксис fetch(..., { next: { tags, revalidate } }).
На self-hosted-деплої потрібен спільний кеш-хендлер (наприклад, Redis через @neshca/cache-handler), інакше інвалідація торкнеться лише одного інстансу.
Вебхуки з CMS зазвичай прив'язують до Route Handler, який викликає revalidateTag для конкретної сутності (а не всього сайту).
Чим відрізняється revalidateTag від revalidatePath?
Найкоротша відповідь: тег є логічною одиницею даних, а шлях є URL. Ви позначаєте закешовані фрагменти тегом (наприклад, product-42, homepage-hero) і потім вибірково їх інвалідуєте. Це радикально краще за revalidatePath, коли одні й ті самі дані використовуються на десятках маршрутів: замість того щоб перелічувати їх усі, ви скидаєте один тег.
А revalidatePath корисний, коли зміна стосується самої URL-структури. Наприклад, ви змінили slug статті, і тепер стара URL має редиректити, а нова має рендеритись. Або ви щойно опублікували чернетку, і хочете, щоб /blog/my-post перебудувалася повністю (з layout, метаданими, sitemap).
У проді я використовую обидва одночасно. У Server Action публікації статті виглядає це так: revalidateTag('posts') оновлює будь-який список постів (головна, категорія, RSS), а revalidatePath('/blog/[slug]', 'page') перебудовує саму сторінку. Один виклик на дані, один на маршрут. Це паттерн, який виживає найдовше.
Ще одна принципова відмінність. У Next.js 16 з активним режимом dynamicIO та явним динамічним рендерингом функція revalidateTag стає єдиним способом позначити межі кешу для довільних асинхронних функцій, а не тільки для fetch. Раніше, у pages-router-епоху, теги працювали виключно з fetch, тому багато команд просто ігнорували їх. У 16 версії правила змінилися.
Як працює revalidateTag: cacheTag, fetch-теги й Server Actions
У Next.js 16 є два стилі оголошення тегів. Перший, старий, через опції fetch:
Другий, новий, через директиву 'use cache' та функцію cacheTag. Цей варіант працює для будь-якої серверної функції, не тільки для HTTP-запитів, а й для викликів БД, Redis, ORM:
// app/lib/products.ts
import { unstable_cacheTag as cacheTag, unstable_cacheLife as cacheLife } from 'next/cache';
import { db } from '@/lib/db';
export async function getProduct(id: string) {
'use cache';
cacheTag(`product-${id}`, 'products');
cacheLife('hours');
return db.product.findUnique({ where: { id } });
}
Коли ви викликаєте revalidateTag('product-42'), Next.js помічає закешовані значення обох варіантів як застарілі. Наступний запит їх переобчислить. Важливо: інвалідація не є синхронною відносно фактичного рендеру. Next.js гарантує, що новий запит побачить нові дані, але «прогріти» кеш ви маєте самостійно (наприклад, через API after() для фонових задач).
Server Action як точка інвалідації є канонічним паттерном. Ось повний приклад для оновлення продукту:
// app/actions/product.ts
'use server';
import { revalidateTag, revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
import { z } from 'zod';
const UpdateSchema = z.object({
id: z.string().uuid(),
price: z.number().positive(),
});
export async function updateProduct(formData: FormData) {
const parsed = UpdateSchema.parse({
id: formData.get('id'),
price: Number(formData.get('price')),
});
await db.product.update({
where: { id: parsed.id },
data: { price: parsed.price },
});
// Точкова інвалідація конкретного продукту
revalidateTag(`product-${parsed.id}`);
// Каталоги, які показують ціни, також мають оновитись
revalidateTag('products');
// Сама сторінка редагування має оновити свою SSR-версію
revalidatePath(`/admin/products/${parsed.id}`, 'page');
}
revalidatePath: коли скидати весь маршрут
Сигнатура: revalidatePath(originalPath: string, type?: 'layout' | 'page'). Другий параметр часто пропускають, але він критичний. За замовчуванням Next.js вважає, що ви передаєте маршрут сторінки, і скидає її RSC-payload. А якщо передати 'layout', інвалідуються всі вкладені сегменти під цим layout'ом.
// Скинути тільки цю сторінку
revalidatePath('/blog/my-first-post');
// Скинути весь розділ /blog і будь-які підмаршрути
revalidatePath('/blog', 'layout');
// Скинути всі сторінки для динамічного сегмента
revalidatePath('/blog/[slug]', 'page');
Форма /blog/[slug] є тим трюком, який я недооцінював у перші місяці на App Router. Ви не підставляєте конкретне значення; ви передаєте шаблон, і Next.js інвалідує всі маршрути, згенеровані для нього. Це рятує, коли у вас, скажімо, 3000 продуктів і бекенд-міграція змінила формат опису: не треба тегувати кожен продукт окремо.
Для тих, хто мігрує з pages-router: revalidatePath не є аналогом res.revalidate(). У pages-router ви ревалідували ISR-сторінку, тобто перебудовували HTML. У App Router перебудовується RSC-payload, а вже клієнт може отримати оновлені частини через streaming або Partial Prerendering, що поєднує статику й динаміку. Ментальна модель зовсім інша, і саме тут я найдовше «переучувався».
Порівняльна таблиця API інвалідації
Критерій
revalidateTag
revalidatePath
Гранулярність
Довільна одиниця даних (тег)
Маршрут або група маршрутів
Джерело реєстрації
cacheTag() або fetch(..., { next: { tags } })
Автоматично, за структурою файлів App Router
Тип даних
Будь-які функції з 'use cache', а також fetch
RSC-payload маршруту повністю
Складність налаштування
Потребує тегування на місці отримання даних
Нуль конфігурації, працює одразу
Використання з CMS
Ідеально: 1 тег на 1 сутність
Погано: треба знати всі маршрути з цією сутністю
Ризик надмірної інвалідації
Низький
Високий, якщо передати 'layout' на корені
Робота на self-hosted
Потрібен спільний кеш-хендлер
Так само, потрібен спільний кеш-хендлер
Інвалідація за вебхуком від Headless CMS
Найпоширеніший продакшн-кейс: редактор натиснув «Publish» у Contentful/Sanity/Strapi, і сторінка має оновитись за секунди. Класичний паттерн, це Route Handler, який слухає вебхук:
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'node:crypto';
function verifySignature(body: string, header: string | null): boolean {
if (!header || !process.env.CMS_WEBHOOK_SECRET) return false;
const hmac = crypto
.createHmac('sha256', process.env.CMS_WEBHOOK_SECRET)
.update(body)
.digest('hex');
// timingSafeEqual захищає від timing-атак
return crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(header));
}
export async function POST(request: NextRequest) {
const raw = await request.text();
const signature = request.headers.get('x-cms-signature');
if (!verifySignature(raw, signature)) {
return NextResponse.json({ ok: false }, { status: 401 });
}
const payload = JSON.parse(raw) as {
entity: 'post' | 'product' | 'author';
id: string;
slug?: string;
};
// Точкова інвалідація за типом сутності
revalidateTag(`${payload.entity}-${payload.id}`);
revalidateTag(`${payload.entity}s`); // множина для списків
return NextResponse.json({ ok: true, revalidated: [payload.entity, payload.id] });
}
Два моменти, на яких я обпікся у минулих проєктах. По-перше, завжди перевіряйте підпис. Публічний ендпоінт без верифікації є запрошенням до DoS: зловмисник може почати смикати вебхук і змусити ваш сайт постійно перебудовуватись. По-друге, не тегуйте 'all' і не викликайте revalidatePath('/', 'layout') у вебхуку. Редактор, який редагує 20 постів за годину, легко покладе кеш під нуль.
Для докладнішого розуміння, коли обрати Route Handler замість Server Action, дивіться моє порівняння Server Actions і Route Handlers у Next.js 16. Коротко: вебхуки, це завжди Route Handler, бо їх викликає зовнішня система, а не форма браузера.
Чому revalidateTag не працює на self-hosted (і як це виправити)
Ось сценарій, який породив половину issues у моїх командах. Сайт на Vercel працює бездоганно, редактор публікує пост і стрічка оновлюється. Перенесли на власний Kubernetes-кластер з 3 репліками, і чверть відвідувачів бачать стару версію. Причина проста: дефолтний in-memory-кеш Next.js локальний для кожного інстансу. revalidateTag шле сигнал тільки поду, який отримав HTTP-запит.
Рішення полягає в підключенні спільного кеш-хендлера. Найпопулярніший, це @neshca/cache-handler з Redis-бекендом. Мінімальна конфігурація:
Після цього revalidateTag в одному поді знімає закешовані значення для всього кластера. Redis-повідомлення, звісно, додає ~1–3 мс до кожного read'у, тому воно виправдане тільки за наявності реального multi-instance-деплою.
Часті помилки та як їх діагностувати
За два роки на App Router я зібрав такий чек-ліст. Проходжусь по ньому щоразу, коли колега пише «інвалідація не працює».
1. Виклик з клієнтського компонента
revalidateTag і revalidatePath є серверними функціями. Якщо ви імпортуєте їх у файл з 'use client', збірка впаде з чіткою помилкою. Але якщо ви робите виклик через useEffect → fetch('/api/...'), помилки не буде, а поведінка виглядатиме дивно. Правильно: виклик усередині Server Action або Route Handler.
2. Тег зареєстрований, але не збігається
Найтипічніша: cacheTag(`product-${id}`) у getter'і, а в Server Action пишуть revalidateTag('product-' + id). Здавалось би, одне й те саме. Але id може бути числом в одному місці й рядком в іншому, тоді конкатенація дасть product-42 проти product-[object Object]. Уніфікуйте типи або зробіть маленьку функцію-фабрику: function productTag(id: string) { return `product-${id}`; } і імпортуйте її обабіч.
3. Забуто про cacheLife
З dynamicIO без явного cacheLife Next.js може взагалі не кешувати значення, і revalidateTag буде no-op'ом. Перевірте, що функція має і 'use cache', і cacheTag(), і cacheLife().
4. HMR у dev'і
У режимі розробки Next.js часто скидає кеш при hot reload. Це заплутує: ви думаєте, що revalidateTag спрацював, а насправді просто перезбіркою кешу. Тестуйте інвалідацію в next build && next start. Там поведінка збігається з продом.
5. Мовчазні помилки в Server Action
Якщо запит до БД у Server Action кине exception доrevalidateTag, інвалідація не станеться. Ставте revalidate* у try/finally тільки якщо ви впевнені, що хочете скидати кеш навіть після помилки. У 90% випадків це неправильно.
// app/actions/publish.ts
'use server';
import { revalidateTag } from 'next/cache';
export async function publishPost(id: string) {
try {
await db.post.update({ where: { id }, data: { status: 'published' } });
} catch (err) {
// Не інвалідуємо: дані не змінилися
console.error('Publish failed', err);
throw err;
}
// Досягли сюди тільки якщо update успішний
revalidateTag(`post-${id}`);
revalidateTag('posts');
}
Чи можу я викликати revalidateTag з клієнтського компонента?
Ні. revalidateTag є серверним API. Викликайте його з Server Action (форма, кнопка з action-атрибутом) або з Route Handler (вебхук, POST-ендпоінт). Клієнт може тригерити Server Action, але саме імпорт функції можливий тільки на сервері.
Скільки часу займає інвалідація після виклику revalidateTag?
На Vercel практично миттєво: сам виклик синхронний, а edge-мережа отримує сигнал за десятки мілісекунд. На self-hosted з Redis-кеш-хендлером додається один network-round-trip до Redis, зазвичай 1–5 мс. Наступний HTTP-запит гарантовано побачить свіжі дані.
Чи потрібно викликати revalidatePath, якщо я вже викликав revalidateTag?
Не завжди. Якщо всі дані на маршруті прив'язані до тегів, і ви скинули відповідні теги, маршрут ререндериться на наступному запиті. revalidatePath потрібен додатково, коли змінилася сама структура маршруту (новий slug, новий layout) або коли на сторінці є не-тегований fetch.
Що робити, якщо revalidateTag не спрацював?
Пройдіться по чек-лісту: тег на кешованій функції збігається з тим, що передається в revalidateTag (по типу і по значенню); функція має 'use cache' і cacheLife; ви тестуєте в next start, а не в next dev; на self-hosted підключено спільний cache-handler. У 90% випадків проблема виявляється у розбіжності рядка тегу.
Чи можна інвалідувати кеш за розкладом (наприклад, щогодини)?
Так, але не через revalidateTag. Використовуйте cacheLife('hours') або { next: { revalidate: 3600 } } у fetch. Це time-based revalidation. revalidateTag призначений для подієвої інвалідації (запис у БД, вебхук), а не для розкладу. Якщо треба обидва режими, комбінуйте.
Чим revalidateTag відрізняється від router.refresh() на клієнті?
router.refresh() є клієнтським викликом, який просить сервер ререндерити поточний маршрут для цього одного користувача, без інвалідації кешу. А revalidateTag глобально позначає закешовані дані як застарілі для всіх майбутніх запитів усіх користувачів. Використовуйте router.refresh() після Server Action, щоб UI миттєво відобразив зміни для поточного клієнта, а revalidateTag, щоб решта користувачів теж побачила свіжі дані.
React Compiler у Next.js 16 автоматично мемоізує клієнтські компоненти. Розбираємо конфіг, режими infer/annotation, роботу з use no memo, бенчмарки бандла й білдів, а також шість типових пасток міграції з реального SaaS у 2026 році.
Практичний гайд зі стрімінгового SSR у Next.js 16. Де саме розставити Suspense-межі, як не зруйнувати LCP, як профілювати у DevTools із React 19 Performance Tracks. З прикладами коду й чек-листом перед деплоєм.
Turbopack у Next.js 16 — стабільний Rust-білдер, що замінив Webpack. Як увімкнути, налаштувати loaders і aliases, використати persistent cache та мігрувати без даунтайму.