Коротка відповідь. Кеш у Next.js перестає бути магією, коли ти міняєш порядок питань. Не «який API викликати?», а спочатку: чиї це дані, наскільки свіжими вони мають бути для цього екрана, і хто має право їх інвалідувати? Відповівши, ти майже автоматично обираєш між трьома інструментами 16.3: revalidateTag(tag, 'max') – «оновиться скоро, користувач не чекає», updateTag(tag) – «користувач щойно сам змінив дані й мусить це побачити», refresh() – «онови поточний екран клієнта без тегів». Усе інше – деталі.

Три шари, які постійно плутають

Коли кажуть «кеш Next.js», ідеться мінімум про три різні речі з різними власниками:

  1. Request memoization. Дедуплікація однакових запитів у межах одного рендера одного запиту. Живе мілісекунди, нічого не треба інвалідувати – це просто «не ходи в базу двічі за один рендер».
  2. Data cache. Серверний кеш результатів: функції та компоненти з 'use cache', позначені тегами через cacheTag('posts'), або fetch(url, { next: { tags: ['posts'] } }). Живе між запитами й користувачами. Саме ним керують revalidateTag/updateTag.
  3. Client router cache. Те, що браузер користувача вже отримав і тримає для миттєвих переходів назад/вперед. Саме через нього «я ж зробив revalidate, а користувач досі бачить старе» – сервер оновився, клієнтський знімок ні.

Баг-репорти про кеш майже завжди зводяться до того, що інвалідували один шар, а дивились на інший.

Спершу freshness-вимога, потім API

Для кожного читання даних у проєкті корисно мати явну відповідь на одне питання: що станеться поганого, якщо користувач побачить ці дані застарілими на хвилину?

  • «Нічого» (блог, каталог, документація) → кешуй агресивно, інвалідуй через revalidateTag(tag, 'max'). Семантика stale-while-revalidate: виклик лише позначає дані протухлими, наступний запит отримує stale-версію миттєво, а свіжа тягнеться у фоні. Важлива деталь із документації: ревалідацію запускає запит, а не сам виклик revalidateTag – сторінки оновлюються в міру відвідування, не всі одразу.
  • «Користувач щойно сам це змінив і дивиться на результат» → це read-your-own-writes, і для нього існує updateTag(tag): дані протухають негайно, наступний запит чекає на свіжі. Працює лише в Server Actions – і це не обмеження, а підказка: такий сценарій і є «користувач натиснув кнопку».
  • «Дані на екрані не тегуються, але екран треба оновити» → refresh() із Server Action оновлює клієнтський роутер для поточного перегляду.

Зверни увагу на зміну в 16.3: стара одноаргументна форма revalidateTag(tag) деприкована. Вона поводилась як { expire: 0 } – блокуючий miss для наступного запиту – і саме нею люди ненавмисно робили собі повільні сторінки. Тепер вибір явний: 'max' для SWR, updateTag для RYOW, { expire: 0 } – лише коли ти поза Server Action (webhook у Route Handler) і дані мусять зникнути негайно.

Приклад: адмінка редагує статтю, публічна сторінка оновлюється

Повний цикл read-your-own-writes у сучасному вигляді:

// app/article/[slug]/page.tsx – читання з тегом
import { cacheTag } from 'next/cache';

async function getArticle(slug: string) {
  'use cache';
  cacheTag(`article-${slug}`, 'articles');
  return db.article.findUnique({ where: { slug } });
}
// app/actions.ts – мутація
'use server';

import { updateTag, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';

export async function saveArticle(slug: string, formData: FormData) {
  await db.article.update({ where: { slug }, data: parse(formData) });

  // Редактор має побачити СВОЮ зміну негайно:
  updateTag(`article-${slug}`);
  // Список статей може оновитись «скоро», без блокування:
  revalidateTag('articles', 'max');

  redirect(`/article/${slug}`);
}

Два різні виклики тут – не випадковість, а сама суть моделі: одна й та сама мутація має різні freshness-вимоги для різних читачів. Автор дивиться на свою статтю – йому updateTag. Випадковий відвідувач списку – йому досить SWR.

Типові баги

  1. Вічний stale. Дані закешовані через 'use cache', але cacheTag забули – і тепер жоден revalidate їх не дістає, бо в них немає імені. Симптом: «інвалідую тег, нічого не змінюється». Перевірка завжди одна: чи тег, який ти інвалідуєш, реально призначений цим даним.
  2. Stale тільки у того, хто натиснув. Мутація через Route Handler + revalidateTag без подальшого оновлення клієнта: сервер уже свіжий, а користувач дивиться на client router cache. У Server Action цю проблему закривають updateTag/refresh у відповіді самої дії.
  3. Cache stampede своїми руками. { expire: 0 } на гарячому тегу означає: наступний запит кожного користувача – блокуючий похід у джерело. На трафіку це та сама лавина, про яку я писав у статті про кешування й Redis, тільки тепер її організував фреймворк за твоєю ж командою. 'max' існує саме щоб цього не було.
  4. Один тег на все. revalidateTag('data', 'max') після кожної мутації – це не кешування, це глобальний reset із зайвими перерендерами. Тег має відповідати власнику даних: article-${slug}, user-${id}, списковий тег окремо.

Чекліст для code review кешування

  • У кожного 'use cache' є cacheTag, і тег згаданий хоча б в одній мутації (інакше навіщо кешували?).
  • Для кожної мутації явно видно freshness-рішення: updateTag (RYOW), revalidateTag + 'max' (SWR) чи refresh – і це рішення можна пояснити словами користувача.
  • { expire: 0 } зустрічається лише в Route Handlers із зовнішніх тригерів, і ніколи – на найгарячіших тегах без коментаря чому.
  • Теги іменовані за власником даних, не за сторінкою.
  • Немає «ритуальних» подвійних інвалідацій того самого тега різними API «про всяк випадок».

Як ці рішення лягають в архітектуру навігації – дивись у свіжому розборі Instant Navigations у Next.js 16.3: кеш і навігація в 16.3 – це фактично одна система, яку варто проєктувати разом. Базовий контекст по рендерингу – у огляді Next.js для React-розробника, а загальні патерни кешів без фреймворків – у frontend system design розборі.

Практичне завдання

Візьми одну мутацію зі свого проєкту й пройди ланцюжок письмово: які дані вона змінює → якими тегами ці дані позначені (перевір, що позначені!) → хто дивиться на ці дані в момент мутації → яка в кожного з них freshness-вимога → який API це виражає. Якщо на будь-якому кроці відповіді немає – ти щойно знайшов місце, де кеш «працює випадково». Поправ і повтори для наступної мутації: після третьої модель сяде в голову назавжди.