# Кеш у Next.js без магії: що саме кешується, коли revalidate, updateTag і refresh справді потрібні

> Ментальна модель кешу Next.js 16.3 замість зубріння API: хто володіє даними, що означає stale, і чому revalidateTag, updateTag і refresh – це три різні відповіді на питання «наскільки свіжим має бути UI».

- Автор: Юра Скиба (https://cookiesoftware.io)
- Опубліковано: 2026-10-01
- Категорія: React і Frontend
- Canonical: https://cookiesoftware.io/blog/nextjs-cache-mental-model

---

**Коротка відповідь.** Кеш у 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 у сучасному вигляді:

```ts
// 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 } });
}
```

```ts
// 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](/blog/redis-koly-potriben-kesh), тільки тепер її організував фреймворк за твоєю ж командою. `'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](/blog/nextjs-instant-navigations): кеш і навігація в 16.3 – це фактично одна система, яку варто проєктувати разом. Базовий контекст по рендерингу – у [огляді Next.js для React-розробника](/blog/nextjs-dlia-react-rozrobnyka), а загальні патерни кешів без фреймворків – у [frontend system design розборі](/blog/frontend-system-design-interview).

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

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