Next.js 16 revalidateTag та revalidatePath: повний гід із інвалідації кешу (2026)

Практичний гід із revalidateTag та revalidatePath у Next.js 16: cacheTag, вебхуки CMS, self-hosted-налаштування з Redis, часті помилки та їх діагностика.

Оновлено: 16 серпня 2026

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:

// app/lib/products.ts
export async function getProduct(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`, {
    next: { tags: [`product-${id}`, 'products'] },
  });
  return res.json();
}

Другий, новий, через директиву '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-бекендом. Мінімальна конфігурація:

// cache-handler.mjs
import { CacheHandler } from '@neshca/cache-handler';
import createRedisHandler from '@neshca/cache-handler/redis-strings';
import { createClient } from 'redis';

const client = createClient({ url: process.env.REDIS_URL });
await client.connect();

CacheHandler.onCreation(async () => {
  const handler = await createRedisHandler({ client, keyPrefix: 'nextjs:' });
  return { handlers: [handler] };
});

export default CacheHandler;
// next.config.ts
import type { NextConfig } from 'next';

const config: NextConfig = {
  cacheHandler: require.resolve('./cache-handler.mjs'),
  cacheMaxMemorySize: 0, // вимикаємо in-memory-кеш
};

export default config;

Після цього revalidateTag в одному поді знімає закешовані значення для всього кластера. Redis-повідомлення, звісно, додає ~1–3 мс до кожного read'у, тому воно виправдане тільки за наявності реального multi-instance-деплою.

Часті помилки та як їх діагностувати

За два роки на App Router я зібрав такий чек-ліст. Проходжусь по ньому щоразу, коли колега пише «інвалідація не працює».

1. Виклик з клієнтського компонента

revalidateTag і revalidatePath є серверними функціями. Якщо ви імпортуєте їх у файл з 'use client', збірка впаде з чіткою помилкою. Але якщо ви робите виклик через useEffectfetch('/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');
}

Офіційна документація по цих API живе на nextjs.org/docs — revalidateTag та revalidatePath. Окремо радив би переглянути GitHub-релізи Next.js, бо семантика тегів помітно уточнювалась між 15.3 і 16.0.

Поширені запитання

Чи можу я викликати 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, щоб решта користувачів теж побачила свіжі дані.

Ben Howard
Про Автора Ben Howard

Full-stack Next.js developer who's been with the framework since pages-only days. Slowly warming up to App Router.