Навигация

Кэширование в Next.js: уровни и ревалидация

Next.js расширяет нативный fetch API, добавляя многоуровневую систему кэширования. Понимание этой системы — ключ к производительности приложений. Этот документ разбирает каждый уровень кэша, их взаимодействие и стратегии управления.

Этот документ — детальное руководство по кэшированию. Базовые концепты Data Fetching описаны в Глубокое погружение в Data Fetching.

Содержание

  1. 4 уровня кэша в Next.js
  2. Data Cache
  3. Request Memoization
  4. Full Route Cache
  5. Router Cache
  6. Взаимодействие всех уровней кэша
  7. Полный цикл кэширования: таймлайн
  8. Ре-валидация кэша
  9. Кэширование vs Хранение
  10. Глобальная настройка кэширования
  11. Сравнение с Nuxt.js
  12. Best Practices
  13. Антипаттерны
  14. Ключевые тезисы для интервью
  15. Заключение
  16. Полезные ссылки

4 уровня кэша в Next.js

В Next.js есть 4 уровня кэша, каждый со своим расположением и временем жизни:

┌─────────────────────────────────────────────────────────────────┐
│  1. REQUEST MEMOIZATION (Сервер, один render)                   │
│     └── Где: Память Node.js процесса                           │
│     └── Время жизни: Один render страницы                      │
│     └── Что: Дедупликация одинаковых fetch внутри одного render │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  2. DATA CACHE (Сервер, persistent)                             │
│     └── Где: Диск сервера (.next/cache/fetch-cache)            │
│     └── Время жизни: Permanent (пока не инвалидирован)         │
│     └── Что: Результаты fetch запросов                         │
│     └── Доступ: Только сервер (Node.js)                        │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  3. FULL ROUTE CACHE (Сервер, persistent)                       │
│     └── Где: Диск сервера (.next/server/app)                   │
│     └── Время жизни: Permanent (для статических страниц)       │
│     └── Что: Готовый HTML + RSC Payload                        │
│     └── Доступ: Только сервер (Node.js)                        │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  4. ROUTER CACHE (Клиент, браузер)                              │
│     └── Где: Память браузера (RAM)                             │
│     └── Время жизни: Сессия (до F5 или router.refresh())       │
│     └── Что: RSC Payload посещённых маршрутов                  │
│     └── Доступ: Только клиент (браузер)                        │
└─────────────────────────────────────────────────────────────────┘

Сводная таблица

КэшГдеЧтоDurationКак управлять
Data CacheСерверfetch responsesPermanent / revalidatecache, next.revalidate, next.tags
Full Route CacheСерверHTML + RSCPermanentdynamic, revalidate
Router CacheКлиентRSC payloadSessionrouter.refresh()
Request MemoizationСерверДедупликация fetchОдин renderАвтоматически

Data Cache

Data Cache — серверный кэш результатов fetch запросов. Это основной механизм, который позволяет избегать повторных HTTP-запросов к API.

Где физически хранится

.next/cache/fetch-cache/
├── hash-of-url-1.json
├── hash-of-url-2.json
└── hash-of-url-3.json

Каждый файл содержит:

{
  "data": { "products": [...] },
  "timestamp": 1234567890
}

Пошаговый процесс

Что происходит при первом запросе:

1. Пользователь → GET /products
   ↓
2. Next.js получает запрос
   ↓
3. Выполняет page.tsx на сервере
   ↓
4. Встречает: await fetch('https://api.example.com/products')
   ↓
5. Проверяет Data Cache:
   ┌──────────────────────────────┐
   │ .next/cache/fetch-cache/     │
   │ └── hash-of-url.json         │ ← Файл не найден
   └──────────────────────────────┘
   ↓
6. Кэш пуст → делает реальный HTTP запрос к API
   ↓
7. Получает response: { products: [...] }
   ↓
8. Сохраняет в Data Cache:
   ┌──────────────────────────────┐
   │ .next/cache/fetch-cache/     │
   │ └── hash-of-url.json         │ ← Создан файл с данными
   │     {                         │
   │       "data": {...},          │
   │       "timestamp": 1234567890 │
   │     }                         │
   └──────────────────────────────┘
   ↓
9. Рендерит HTML с данными
   ↓
10. Отправляет HTML клиенту

Что происходит при повторном запросе (кэш найден):

1. Пользователь → GET /products
   ↓
2. Next.js получает запрос
   ↓
3. Выполняет page.tsx на сервере
   ↓
4. Встречает: await fetch('https://api.example.com/products')
   ↓
5. Проверяет Data Cache:
   ┌──────────────────────────────┐
   │ .next/cache/fetch-cache/     │
   │ └── hash-of-url.json         │ ← Файл найден!
   └──────────────────────────────┘
   ↓
6. Проверяет срок действия:
   - Если cache: 'force-cache' → используем кэш (бессрочно)
   - Если next: { revalidate: 60 } → проверяем timestamp
     - Если прошло < 60 сек → используем кэш
     - Если прошло >= 60 сек → делаем новый запрос (фоновая ревалидация)
   ↓
7. Кэш валиден → читаем данные из файла
   ↓
8. НЕ делаем HTTP запрос к API (экономия ресурсов)
   ↓
9. Рендерит HTML с кэшированными данными
   ↓
10. Отправляет HTML клиенту

Сценарии поведения

Сценарий 1: Data Cache (force-cache)

const data = await fetch('https://api.example.com/data').then(r => r.json())
Первый запрос:
  1. Проверка Data Cache → не найден
  2. Реальный HTTP запрос к API
  3. Сохранение в Data Cache (навсегда)
  4. Возврат данных

Второй запрос (через день):
  1. Проверка Data Cache → найден
  2. Чтение из файла .next/cache/fetch-cache/hash.json
  3. Возврат кэшированных данных (HTTP запрос НЕ делается)
  
Результат: мгновенный ответ, нет нагрузки на API

Сценарий 2: Data Cache с revalidate

const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 }
}).then(r => r.json())
Первый запрос (T=0):
  1. Проверка Data Cache → не найден
  2. Реальный HTTP запрос к API
  3. Сохранение в Data Cache с timestamp
  4. Возврат данных

Второй запрос (T=30сек):
  1. Проверка Data Cache → найден
  2. Проверка timestamp: прошло 30сек < 60сек → валиден
  3. Возврат кэшированных данных

Третий запрос (T=61сек):
  1. Проверка Data Cache → найден
  2. Проверка timestamp: прошло 61сек >= 60сек → устарел
  3. Возврат кэшированных данных (пользователь видит старые)
  4. Фоновая ревалидация:
     - Реальный HTTP запрос к API
     - Обновление Data Cache
     - Следующий запрос получит новые данные
     
Результат: пользователь всегда видит данные (старые или новые),
           нет задержки на запрос к API

Сценарий 3: no-store (без кэша)

const data = await fetch('https://api.example.com/data', {
  cache: 'no-store'
}).then(r => r.json())
Каждый запрос:
  1. Проверка Data Cache → пропускается (no-store)
  2. Реальный HTTP запрос к API
  3. Возврат данных (кэш НЕ сохраняется)
  
Результат: всегда актуальные данные, нагрузка на API

Request Memoization

В React-приложениях компоненты часто композируются из независимых частей. Каждый компонент может запрашивать данные, которые ему нужны, не зная о соседях:

// app/dashboard/page.tsx
import { UserStats } from './UserStats'
import { UserAvatar } from './UserAvatar'

export default async function Dashboard({ params }: { params: { id: string } }) {
  return (
    <div>
      <UserStats userId={params.id} />
      <UserAvatar userId={params.id} />
    </div>
  )
}

// app/dashboard/UserStats.tsx
async function getUser(id: string) {
  return fetch(`https://api.example.com/users/${id}`).then(r => r.json())
}

export async function UserStats({ userId }: { userId: string }) {
  const user = await getUser(userId)
  return <div>Постов: {user.postsCount}</div>
}

// app/dashboard/UserAvatar.tsx
async function getUser(id: string) {
  return fetch(`https://api.example.com/users/${id}`).then(r => r.json())
}

export async function UserAvatar({ userId }: { userId: string }) {
  const user = await getUser(userId)
  return <img src={user.avatarUrl} />
}

Без memoization: 2 HTTP-запроса к одному URL (каждый компонент делает свой запрос).

С memoization: 1 HTTP-запрос. Next.js видит одинаковый URL и возвращает тот же Promise.

Почему это не ошибка программиста

Передача данных через props потребовала бы изменения архитектуры:

// Альтернатива без memoization — родитель загружает данные
export default async function Dashboard({ params }: { params: { id: string } }) {
  const user = await getUser(params.id) // Родитель загружает
  
  return (
    <div>
      <UserStats user={user} /> {/* Передаём через props */}
      <UserAvatar user={user} /> {/* Передаём через props */}
    </div>
  )
}

Проблемы этого подхода:

  • Родитель должен знать, какие данные нужны детям
  • Компоненты теряют самостоятельность (не переиспользовать в другом месте)
  • Нарушается принцип “каждый компонент загружает свои данные”

Реальные сценарии

Сценарий 1: Layout и Page

// app/layout.tsx
export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const user = await getCurrentUser() // Запрос #1 (для навигации)
  
  return (
    <html>
      <body>
        <Header user={user} />
        {children}
      </body>
    </html>
  )
}

// app/profile/page.tsx
export default async function ProfilePage() {
  const user = await getCurrentUser() // Запрос #2 (тот же URL)
  
  return <Profile user={user} />
}

Layout не знает, какая страница внутри него. Страница не знает, что layout уже загрузил пользователя. Memoization предотвращает дублирование без изменения архитектуры.

Сценарий 2: Вложенные компоненты

// app/product/page.tsx
export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id)
  
  return (
    <div>
      <ProductHeader product={product} />
      <ProductDetails productId={params.id} />
    </div>
  )
}

// app/product/ProductDetails.tsx
export async function ProductDetails({ productId }: { productId: string }) {
  const product = await getProduct(productId) // Тот же URL
  
  return (
    <div>
      <p>{product.description}</p>
      <ProductReviews productId={productId} />
    </div>
  )
}

ProductDetails может использоваться на других страницах без product в props. Компонент самозависимый.

Когда это действительно ошибка

// ❌ Дублирование в одном компоненте — бессмысленно
export default async function Page() {
  const user1 = await fetch('/api/me').then(r => r.json())
  const user2 = await fetch('/api/me').then(r => r.json())
  
  return <div>{user1.name}</div>
}

Memoization спасает от этого, но лучше просто не писать так.

Как это работает

Рендер страницы:
  UserStats: getUser('1')  →  fetch('.../users/1')  →  Promise A
  UserAvatar: getUser('1') →  Проверка memoization  →  Возврат Promise A (тот же)

Результат: только ОДИН HTTP запрос, оба компонента получают данные

Где хранится: В памяти Node.js процесса, только на время одного render.

Важно: Request Memoization работает только для одинаковых fetch вызовов с одинаковыми URL и опциями. Если URL отличается — это разные запросы.

Full Route Cache

Для статических страниц Next.js кэширует весь HTML + RSC Payload:

┌─────────────────────────────────────────────────────────────┐
│  .next/server/app/                                           │
│  ├── products/                                               │
│  │   ├── page_client-reference-manifest.js                  │
│  │   └── page.html  ← Готовый HTML страницы                 │
│  │   └── page.rsc   ← RSC Payload (сериализованный tree)    │
│  └── blog/                                                   │
│      └── page.html                                           │
│      └── page.rsc                                            │
└─────────────────────────────────────────────────────────────┘

Что происходит:

1. Build time или первый запрос (для ISR)
   ↓
2. Next.js рендерит страницу полностью
   ↓
3. Сохраняет:
   - HTML → .next/server/app/products/page.html
   - RSC Payload → .next/server/app/products/page.rsc
   ↓
4. При следующем запросе:
   - Читает готовый HTML с диска
   - НЕ выполняет page.tsx заново
   - НЕ делает fetch запросы
   - Отправляет готовый HTML

Когда используется:

  • Статические страницы (нет cookies(), headers(), searchParams)
  • ISR страницы (после revalidation)

Когда НЕ используется:

  • Динамические страницы (есть cookies(), headers())
  • fetch с cache: 'no-store'
  • export const dynamic = 'force-dynamic'
// Отключить Full Route Cache для конкретной страницы
export const dynamic = 'force-dynamic'

export default async function Page() {
  const data = await fetch('https://api.example.com/data', {
    cache: 'no-store'
  }).then(r => r.json())
  
  return <div>{data.value}</div>
}

Router Cache

Router Cache — клиентский кэш RSC Payload в браузере. Включён по умолчанию, не требует настройки.

Что кэшируется

При навигации по сайту Next.js сохраняет RSC Payload (сериализованный React tree) каждого посещённого маршрута в памяти браузера:

┌─────────────────────────────────────────────────────────────┐
│  Браузер (Память)                                            │
│                                                              │
│  Router Cache:                                               │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  / → RSC Payload (сериализованный React tree)          │ │
│  │  /products → RSC Payload                               │ │
│  │  /products/123 → RSC Payload                           │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                              │
│  Время жизни: до F5 или router.refresh()                    │
└─────────────────────────────────────────────────────────────┘

Включён по умолчанию

Router Cache работает автоматически для всех навигаций через <Link> или router.push(). Никакой конфигурации не требуется.

Время жизни

СобытиеЧто происходит
F5 / Ctrl+RRouter Cache очищается полностью
router.refresh()Router Cache очищается, данные перезагружаются
Закрытие вкладкиКэш теряется (это память браузера, не диск)
Переход по прямой ссылкеКэш не используется (новый entry point)

Когда Router Cache используется

Сценарий 1: Возврат на посещённую страницу

1. Пользователь на /products
   ↓
2. Кликает на /products/123 → загружается, сохраняется в Router Cache
   ↓
3. Возвращается назад на /products/123
   ↓
4. Router Cache: найден → мгновенная навигация (нет запроса к серверу)

Сценарий 2: Навигация через <Link> с prefetch

import Link from 'next/link'

export function Navigation() {
  return (
    <Link href="/products">Товары</Link>
  )
}

Когда ссылка появляется в viewport, Next.js автоматически prefetch RSC Payload и сохраняет в Router Cache. При клике — мгновенная навигация.

Статические vs динамические страницы

Статические страницы (кэшируются в Full Route Cache):

1. Пользователь кликает на /about
   ↓
2. Router Cache: не найден
   ↓
3. Запрос к серверу → Full Route Cache: найден
   ↓
4. Сервер возвращает готовый RSC Payload (без выполнения page.tsx)
   ↓
5. Сохраняется в Router Cache

Динамические страницы (с cookies(), headers(), no-store):

1. Пользователь кликает на /profile
   ↓
2. Router Cache: не найден
   ↓
3. Запрос к серверу → Full Route Cache: пропущен
   ↓
4. Сервер выполняет page.tsx, делает fetch с no-store
   ↓
5. Возвращает RSC Payload
   ↓
6. Сохраняется в Router Cache (но только для этой сессии)

Важно: Router Cache работает одинаково для статических и динамических страниц. Разница в том, что динамические страницы не кэшируются на сервере (Full Route Cache), но всё равно кэшируются в браузере после первого посещения.

Prefetching и Router Cache

App Router (Next.js 13+):

Next.js автоматически prefetch ссылки, которые появляются в viewport (в production mode):

// Когда ссылка появляется на экране, Next.js prefetch RSC Payload
<Link href="/products/123">Товар 123</Link>

// При клике — мгновенная навигация из Router Cache

Как это работает:

1. Пользователь скроллит страницу
   ↓
2. Ссылка /products/123 появляется в viewport
   ↓
3. Next.js автоматически prefetch RSC Payload
   ↓
4. Сохраняется в Router Cache
   ↓
5. При клике — мгновенная навигация (нет запроса к серверу)

Pages Router (старое поведение):

В Pages Router prefetch происходит только при наведении мыши или фокусе на ссылку:

// Pages Router: prefetch только при hover/focus
<Link href="/products/123">Товар 123</Link>

RSC Payload — это сериализованное представление React Server Components tree, которое сервер отправляет клиенту. Содержит структуру компонентов, их props и результаты серверных fetch запросов. Клиент использует RSC Payload для гидратации и рендеринга без повторного выполнения серверного кода.

Отключить prefetch:

<Link href="/products/123" prefetch={false}>
  Товар 123
</Link>

Важно: Prefetch в viewport работает только в production mode. В development mode prefetch происходит только при наведении, чтобы избежать лишней нагрузки на сервер при разработке.

Как очистить Router Cache

Router Cache нужно очищать, когда:

  • Данные изменились через мутацию (Server Actions)
  • Данные обновляются в реальном времени
  • Важно видеть актуальное состояние (админ-панели, дашборды)

Router Cache НЕ мешает, когда:

  • Контент статический и редко меняется
  • Пользователь сам обновляет страницу (F5)
  • Данные персонализированы и не кэшируются на сервере

Способ 1: router.refresh()

'use client'

import { useRouter } from 'next/navigation'

export function RefreshButton() {
  const router = useRouter()

  return (
    <button onClick={() => {
      router.refresh()
    }}>
      Обновить данные
    </button>
  )
}

Способ 2: После мутаций (Server Actions)

'use server'

import { revalidatePath } from 'next/cache'

export async function updateItem(formData: FormData) {
  await db.item.update({ /* ... */ })
  revalidatePath('/items')
}

// На клиенте
'use client'
import { useRouter } from 'next/navigation'

export function UpdateForm() {
  const router = useRouter()
  
  return (
    <form action={async (formData) => {
      await updateItem(formData)
      router.refresh() // Очищает Router Cache
    }}>
      {/* ... */}
    </form>
  )
}

Способ 3: router.push() с replace

'use client'

import { useRouter } from 'next/navigation'

export function DeleteButton() {
  const router = useRouter()

  const handleDelete = async () => {
    await deleteItem()
    router.push('/items') // Переход на страницу (обновит Router Cache)
  }

  return <button onClick={handleDelete}>Удалить</button>
}

Когда Router Cache может мешать

Проблема: Данные не обновляются после мутации

// ❌ Плохо: после мутации пользователь видит старые данные
'use server'

export async function updateItem(formData: FormData) {
  await db.item.update({ /* ... */ })
  revalidatePath('/items') // Очищает серверные кэши
  // Но Router Cache в браузере остаётся!
}

// ✅ Хорошо: очищаем Router Cache на клиенте
'use client'
import { useRouter } from 'next/navigation'

export function UpdateForm() {
  const router = useRouter()
  
  return (
    <form action={async (formData) => {
      await updateItem(formData)
      router.refresh() // Очищает Router Cache
    }}>
      {/* ... */}
    </form>
  )
}

Проблема: Пользователь видит устаревшие данные

Если пользователь долго находится на сайте, Router Cache может содержать устаревшие данные. Решение:

'use client'

import { useEffect } from 'react'
import { useRouter } from 'next/navigation'

export function AutoRefresh() {
  const router = useRouter()

  useEffect(() => {
    // Обновляем данные каждые 5 минут
    const interval = setInterval(() => {
      router.refresh()
    }, 5 * 60 * 1000)

    return () => clearInterval(interval)
  }, [router])

  return null
}

Router Cache и searchParams

Router Cache не кэширует разные searchParams как разные страницы:

// app/products/page.tsx
export default function ProductsPage({
  searchParams
}: {
  searchParams: { category?: string }
}) {
  // /products?category=electronics
  // /products?category=clothing
  
  return <ProductList category={searchParams.category} />
}

Что происходит:

1. Пользователь на /products?category=electronics
   ↓
2. Переходит на /products?category=clothing
   ↓
3. Router Cache: /products → найден (тот же маршрут)
   ↓
4. Но searchParams изменились → сервер всё равно вызывается
   ↓
5. Router Cache обновляется для новых searchParams

Важно: Router Cache кэширует по маршруту, но при изменении searchParams сервер всё равно вызывается для получения новых данных.

Отличие от Full Route Cache

АспектRouter CacheFull Route Cache
ГдеБраузер (клиент)Сервер (диск)
ЧтоRSC PayloadHTML + RSC Payload
КогдаПри навигацииПри build / первом запросе
Время жизниСессия (до F5)Permanent (для статических)
Для когоОдин пользовательВсе пользователи
Как очиститьrouter.refresh()revalidatePath()

Сценарии использования

Когда Router Cache полезен:

  • Частая навигация между страницами (dashboard, admin panel)
  • Возврат на ранее посещённые страницы (кнопка “назад”)
  • Prefetching ссылок для мгновенной навигации

Когда Router Cache может мешать:

  • Данные часто меняются (real-time приложения)
  • После мутаций (нужен router.refresh())
  • Персонализированный контент (нужен no-store на сервере)

Взаимодействие всех уровней кэша

┌─────────────────────────────────────────────────────────────────┐
│                        ЗАПРОС К СЕРВЕРУ                          │
│                                                                  │
│  1. Router Cache (Клиент)                                       │
│     └── Если найден → используется (нет запроса к серверу)      │
│     └── Если не найден → запрос к серверу                       │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                        СЕРВЕР                                    │
│                                                                  │
│  2. Full Route Cache                                            │
│     └── Если найден → возвращается готовый HTML                 │
│     └── Если не найден → выполняется page.tsx                   │
│                                                                  │
│  3. Request Memoization (внутри одного render)                  │
│     └── Дедупликация одинаковых fetch                           │
│                                                                  │
│  4. Data Cache                                                  │
│     └── Если найден и валиден → используются кэшированные данные│
│     └── Если не найден или устарел → реальный HTTP запрос       │
│     └── Результат сохраняется в Data Cache                      │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                        ОТВЕТ КЛИЕНТУ                             │
│                                                                  │
│  5. Сохранение в Router Cache                                   │
│     └── RSC Payload сохраняется для будущих навигаций           │
└─────────────────────────────────────────────────────────────────┘

Полный цикл кэширования: таймлайн

// app/products/page.tsx
export const revalidate = 3600

export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products', {
    next: { 
      revalidate: 3600,
      tags: ['products']
    }
  }).then(r => r.json())

  return <ProductList products={products} />
}

Таймлайн:

T=0:     Первый запрос
         ↓
         Full Route Cache: пуст → выполняется page.tsx
         Data Cache: пуст → реальный fetch к API
         ↓
         Результат:
         - Full Route Cache: сохранён HTML + RSC
         - Data Cache: сохранён response от API
         - Router Cache: сохранён RSC Payload
         
T=1мин:  Второй запрос (тот же пользователь)
         ↓
         Router Cache: найден → используется (нет запроса к серверу)
         
T=5мин:  Новый пользователь → GET /products
         ↓
         Full Route Cache: найден → возвращается готовый HTML
         (page.tsx НЕ выполняется, fetch НЕ делается)
         
T=1час:  Прошёл revalidate=3600
         ↓
         Full Route Cache: устарел
         ↓
         Запрос → выполняется page.tsx
         ↓
         Data Cache: устарел (прошло 3600 сек)
         ↓
         Реальный fetch к API → новые данные
         ↓
         Обновление:
         - Full Route Cache: обновлён
         - Data Cache: обновлён
         
T=1час+1мин:  Server Action: revalidateTag('products')
         ↓
         Data Cache: инвалидирован (независимо от TTL)
         Full Route Cache: инвалидирован
         ↓
         Следующий запрос → реальный fetch → новые данные

Ре-валидация кэша

Ре-валидация — процесс обновления кэшированных данных до истечения их срока жизни. Next.js предоставляет два механизма: по таймеру (автоматически) и по событию (вручную).

Time-based revalidation

Что это: Автоматическое обновление кэша через заданный интервал времени (в секундах). Когда кэш устаревает, Next.js возвращает старые данные пользователю, а в фоне делает новый запрос и обновляет кэш. Следующий пользователь получит уже свежие данные.

Когда применять:

  • Данные меняются предсказуемо и не критично к задержке (каталог товаров, список статей, курсы валют)
  • Нет возможности отслеживать изменения на стороне источника данных
  • Хочется снизить нагрузку на API, жертвуя актуальностью на секунды/минуты

Как применять:

// Вариант 1: через опцию fetch
export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 }
  }).then(r => r.json())

  return <ProductList products={products} />
}

// Вариант 2: через segment config (применяется ко всем fetch на странице)
export const revalidate = 60

export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products').then(r => r.json())
  return <ProductList products={products} />
}
T=0:     Первый запрос → реальный fetch → кэш сохранён
T=30:    Второй запрос → кэш валиден (30 < 60) → возврат из кэша
T=61:    Третий запрос → кэш устарел (61 >= 60):
         1. Пользователь получает СТАРЫЕ данные из кэша (мгновенно)
         2. В фоне → реальный fetch к API → обновление кэша
         3. Следующий запрос получит НОВЫЕ данные

Важно: Time-based revalidation работает по принципу stale-while-revalidate — пользователь никогда не ждёт запрос к API, но может получить слегка устаревшие данные.

On-demand revalidation

Что это: Ручная инвалидация кэша по событию. Вы сами решаете, когда данные устарели — например, после создания, обновления или удаления записи в БД. Кэш удаляется немедленно, и следующий запрос получит свежие данные.

Когда применять:

  • Данные меняются через пользовательские действия (CRUD-операции в админке)
  • Актуальность данных критична (цены, наличие, статусы заказов)
  • Есть webhook или событие от CMS/источника данных
  • Нужно обновить кэш сразу после публикации статьи/товара

Как применять:

// app/actions/revalidate.ts
'use server'

import { revalidatePath } from 'next/cache'
import { revalidateTag } from 'next/cache'

export async function revalidateProducts() {
  revalidatePath('/products')
}

export async function revalidateProductsByTag() {
  revalidateTag('products')
}

revalidatePath

Что это: Инвалидирует Data Cache и Full Route Cache для конкретного URL-пути. Все кэшированные данные и отрендеренный HTML для этого пути удаляются.

Когда применять:

  • Простая структура данных — одна страница = один источник данных
  • CRUD-операции, где чётко известно, какая страница устарела
  • Нужно обновить конкретный маршрут после мутации

Как применять:

// app/actions/update-product.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function updateProduct(formData: FormData) {
  const id = formData.get('id') as string
  const name = formData.get('name') as string

  await db.product.update({
    where: { id },
    data: { name },
  })

  revalidatePath('/products')
  revalidatePath(`/products/${id}`)
}
revalidatePath('/products')       → инвалидирует /products
revalidatePath('/products/123')   → инвалидирует /products/123
revalidatePath('/', 'layout')     → инвалидирует корневой layout и все вложенные страницы

Важно: revalidatePath инвалидирует Full Route Cache и Data Cache для указанного пути, но не очищает Router Cache в браузере пользователя. Для этого нужен router.refresh() на клиенте.

revalidateTag

Что это: Инвалидирует Data Cache для всех fetch-запросов, помеченных указанным тегом. Работает независимо от того, на каких страницах используются эти запросы — один тег может объединять данные с разных маршрутов.

Когда применять:

  • Одни и те же данные используются на нескольких страницах (товары на главной, в каталоге, в рекомендациях)
  • Сложные зависимости — один тег объединяет несколько fetch с разных источников
  • Нужна гранулярная инвалидация по логическим группам данных, а не по URL
  • Данные обновляются через webhook (CMS прислал событие «статья опубликована»)

Как применять:

// app/products/page.tsx — помечаем кэш тегом
export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products', {
    next: { tags: ['products'] }
  }).then(r => r.json())

  return <ProductList products={products} />
}

// app/home/page.tsx — те же данные, тот же тег
export default async function HomePage() {
  const featured = await fetch('https://api.example.com/products?featured=true', {
    next: { tags: ['products'] }
  }).then(r => r.json())

  return <FeaturedProducts products={featured} />
}

// app/actions/update-product.ts — инвалидируем по тегу
'use server'

import { revalidateTag } from 'next/cache'

export async function updateProduct(formData: FormData) {
  await db.product.update({ /* ... */ })

  revalidateTag('products')
}
revalidateTag('products')  → инвалидирует ВСЕ fetch с тегом 'products'
                             независимо от страницы: /products, /home, /recommendations

Совет: Один fetch может иметь несколько тегов: next: { tags: ['products', 'homepage'] }. Это позволяет инвалидировать данные разными событиями.

revalidatePath vs revalidateTag

АспектrevalidatePathrevalidateTag
Что инвалидируетКонкретные пути URLВсе fetch с указанным тегом
ГранулярностьПо маршрутуПо логической группе данных
Связь с источникомПривязана к URL страницыПривязана к данным, а не к странице
МасштабОдин или несколько путейВсе страницы, где используется тег
ИспользованиеПростые CRUDСложные зависимости данных
ПримерrevalidatePath('/products')revalidateTag('products')
// revalidatePath — инвалидирует все страницы по пути
revalidatePath('/products')
revalidatePath('/products/123')

// revalidateTag — инвалидирует все fetch с тегом
// Независимо от того, на какой странице они используются
revalidateTag('products')

Когда что выбрать:

Данные живут на одной странице?
  → revalidatePath('/products')

Одни данные используются на 3+ страницах?
  → revalidateTag('products')

Нужна точечная инвалидация конкретного товара?
  → revalidatePath('/products/123')

Webhook от CMS говорит «обнови всё связанное с товарами»?
  → revalidateTag('products')

Кэширование vs Хранение

В Next.js есть два уровня работы с данными:

Data Cache (кэширование данных)

┌─────────────────────────────────────────────────────────┐
│                   Data Cache                              │
│                                                           │
│  fetch(url, { cache: 'force-cache' })                    │
│  fetch(url, { next: { revalidate: 60 } })                │
│                                                           │
│  Где: Сервер (persistent across requests)                 │
│  Что: Результат fetch запросов                            │
│  Когда: При build и runtime                               │
│  Как работает: Кэширует response от fetch                  │
└─────────────────────────────────────────────────────────┘

Request Memoization (мемоизация запросов)

async function getUser(id: string) {
  return fetch(`https://api.example.com/users/${id}`).then(r => r.json())
}

export default async function Page() {
  const [user1, user2] = await Promise.all([
    getUser('1'),
    getUser('1'),
  ])

  return <div>{user1.name}</div>
}

Full Route Cache

┌─────────────────────────────────────────────────────────┐
│                  Full Route Cache                         │
│                                                           │
│  Кэширует: HTML + RSC Payload целиком                    │
│  Где: Сервер                                              │
│  Когда: При build (статические страницы)                  │
│  Как отключить:                                           │
│    - cookies() / headers() в серверном компоненте         │
│    - fetch с cache: 'no-store'                            │
│    - export const dynamic = 'force-dynamic'               │
└─────────────────────────────────────────────────────────┘

Router Cache (клиентский)

┌─────────────────────────────────────────────────────────┐
│                  Router Cache (Браузер)                   │
│                                                           │
│  Кэширует: RSC Payload visited routes                    │
│  Где: Клиент (браузер)                                   │
│  Когда: При навигации                                     │
│  Duration: Сессия (до обновления страницы)                │
│  Как очистить: router.refresh()                          │
└─────────────────────────────────────────────────────────┘

Глобальная настройка кэширования

// next.config.js
module.exports = {
  experimental: {
    // В Next.js 15+ fetch по умолчанию не кэшируется при build
    // Для кэширования нужно явно указывать cache: 'force-cache'
    // или next: { revalidate: ... }
  }
}

Важно: В Next.js 15+ поведение по умолчанию изменилось. fetch больше не кэшируется по умолчанию при build. Для кэширования нужно явно указывать cache: 'force-cache' или next: { revalidate: ... }.

Сравнение с Nuxt.js

Nuxt: В Nuxt кэширование работает иначе. useFetch автоматически кэширует данные в payload (передаётся с SSR). На клиенте данные восстанавливаются из payload без повторного запроса. Для HTTP-кэширования используется useFetch(url, { $fetch: { headers: { 'Cache-Control': '...' } } }) или Nitro caching rules в nuxt.config.ts. Нет встроенного Data Cache как в Next.js — кэширование делегируется CDN/браузеру или серверным middleware.

Next.js:

// Кэш по умолчанию (force-cache)
const data = await fetch('/api/data').then(r => r.json())

// Без кэша
const data = await fetch('/api/data', { cache: 'no-store' }).then(r => r.json())

// Кэш с revalidation
const data = await fetch('/api/data', { next: { revalidate: 60 } }).then(r => r.json())

// On-demand revalidation
revalidatePath('/data')
revalidateTag('data')

Nuxt:

// useFetch автоматически кэширует в payload (SSR → клиент)
const { data } = await useFetch('/api/data')

// Без кэша (каждый раз новый запрос)
const { data } = await useFetch('/api/data', { 
  $fetch: { headers: { 'Cache-Control': 'no-store' } } 
})

// Серверное кэширование через routeRules (nuxt.config.ts)
export default defineNuxtConfig({
  routeRules: {
    '/api/data': { swr: true },
    '/blog/**': { isr: 3600 },
    '/static/**': { static: true },
  }
})

// Обновление данных на клиенте
await refreshNuxtData('data-key')

Ключевые отличия:

  • Next.js имеет Data Cache — серверный кэш fetch-запросов с granular control
  • Nuxt кэширует данные в payload (передаётся с SSR) и делегирует HTTP-кэширование Nitro
  • Next.js: revalidatePath / revalidateTag для on-demand revalidation
  • Nuxt: refreshNuxtData() для клиентского обновления, routeRules.isr для серверного

Nuxt: В Nuxt нет прямого аналога revalidatePath / revalidateTag. Для обновления данных используется: 1) refreshNuxtData() — принудительно перезагружает данные на клиенте; 2) clearNuxtData() — очищает кэш; 3) Nitro caching rules (routeRules в nuxt.config.ts) для серверного кэширования с swr, static, isr. ISR в Nuxt настраивается через routeRules: { '/blog/**': { isr: 3600 } }.

Best Practices

1. Выбирай стратегию кэширования осознанно

// Статические данные — кэшируем навсегда
const features = await fetch('https://api.example.com/features', {
  cache: 'force-cache',
}).then(r => r.json())

// Динамические данные — без кэша
const user = await fetch('https://api.example.com/me', {
  cache: 'no-store',
  headers: { Authorization: `Bearer ${token}` },
}).then(r => r.json())

// Редко меняющиеся данные — с revalidate
const products = await fetch('https://api.example.com/products', {
  next: { revalidate: 3600 },
}).then(r => r.json())

2. Используй теги для гранулярной инвалидации

// Страница товаров
const products = await fetch('/api/products', {
  next: { tags: ['products'] }
}).then(r => r.json())

// Страница рекомендаций (использует те же данные)
const recommendations = await fetch('/api/products?featured=true', {
  next: { tags: ['products'] }
}).then(r => r.json())

// При обновлении — инвалидируем все сразу
revalidateTag('products')

3. Разделяй статический и динамический контент

import { Suspense } from 'react'

export default async function HomePage() {
  return (
    <div>
      {/* Статический — кэшируется */}
      <HeroSection />

      {/* Динамический — без кэша */}
      <Suspense fallback={<FeedSkeleton />}>
        <UserFeed />
      </Suspense>
    </div>
  )
}

4. Очищай Router Cache после мутаций

'use client'

import { useRouter } from 'next/navigation'

export function DeleteButton({ id }: { id: string }) {
  const router = useRouter()

  const handleDelete = async () => {
    await deleteItem(id)
    router.refresh()
  }

  return <button onClick={handleDelete}>Удалить</button>
}

Антипаттерны

1. revalidate: 0

// ❌ Бессмысленно — кэш сразу невалиден
const data = await fetch('/api/data', {
  next: { revalidate: 0 }
}).then(r => r.json())

// ✅ Используй no-store для динамических данных
const data = await fetch('/api/data', {
  cache: 'no-store'
}).then(r => r.json())

// ✅ Или revalidate с разумным TTL
const data = await fetch('/api/data', {
  next: { revalidate: 60 }
}).then(r => r.json())

2. Забывать про Router Cache

// ❌ После мутации данные не обновятся из-за Router Cache
export async function updateItem(formData: FormData) {
  await db.item.update({ /* ... */ })
  revalidatePath('/items')
  // Пользователь может не увидеть изменения!
}

// ✅ Добавь router.refresh() на клиенте
'use client'
import { useRouter } from 'next/navigation'

export function UpdateForm() {
  const router = useRouter()
  
  return (
    <form action={async (formData) => {
      await updateItem(formData)
      router.refresh()
    }}>
      {/* ... */}
    </form>
  )
}

3. Кэшировать данные пользователя

// ❌ Данные пользователя в кэше — другие пользователи увидят чужие данные
export default async function ProfilePage() {
  const user = await fetch('https://api.example.com/me').then(r => r.json())
  return <div>{user.name}</div>
}

// ✅ Всегда no-store для данных конкретного пользователя
export default async function ProfilePage() {
  const token = cookies().get('auth-token')?.value
  const user = await fetch('https://api.example.com/me', {
    cache: 'no-store',
    headers: { Authorization: `Bearer ${token}` },
  }).then(r => r.json())
  return <div>{user.name}</div>
}

4. Один тег для всех данных

// ❌ Слишком широкий тег — инвалидирует всё
revalidateTag('data')

// ✅ Гранулярные теги
revalidateTag('products')
revalidateTag('categories')
revalidateTag('user-profile')

Ключевые тезисы для интервью

  • Next.js имеет четыре уровня кэша: Request Memoization, Data Cache, Full Route Cache и Router Cache, каждый со своим местом хранения и временем жизни.
  • Request Memoization дедуплицирует одинаковые fetch-запросы внутри одного рендера, хранясь в памяти Node.js только на время render.
  • Data Cache сохраняет результаты fetch на диске сервера, позволяя избегать повторных HTTP-запросов между разными запросами пользователей.
  • Full Route Cache хранит готовый HTML + RSC Payload для статических страниц, возвращаясь без повторного выполнения page.tsx.
  • Router Cache находится в памяти браузера и ускоряет навигацию между посещёнными маршрутами, живя до F5 или router.refresh().
  • Time-based revalidation обновляет кэш по таймеру по модели stale-while-revalidate: пользователь получает старые данные мгновенно, а фоновый запрос обновляет кэш.
  • On-demand revalidation через revalidatePath или revalidateTag позволяет сбрасывать кэш по событию, например после CRUD-операций.
  • revalidatePath инвалидирует кэш по URL, а revalidateTag — по логической группе данных независимо от страницы.
  • revalidatePath и revalidateTag очищают серверные кэши, но не Router Cache в браузере; для этого нужен router.refresh().
  • Статические данные кэшируются навсегда, динамические данные пользователя должны использовать cache: ‘no-store’, а редко меняющиеся — next.revalidate.
  • Использование широких тегов вроде ‘data’ и revalidate: 0 усложняет инвалидацию и ведёт к неожиданному поведению.

Заключение

Кэширование в Next.js — это многоуровневая система, где каждый уровень решает свою задачу: от дедупликации запросов внутри одного рендера до ускорения навигации в браузере. Понимание разницы между Data Cache, Full Route Cache и Router Cache позволяет выбирать правильную стратегию для статических, динамических и персонализированных данных. Time-based и on-demand ревалидация дают гибкость в управлении актуальностью контента. Главное — не забывать очищать Router Cache после мутаций и не кэшировать данные конкретного пользователя на сервере. Попробуйте явно задать cache и revalidate в своих fetch-запросах и проверьте, какие кэши участвуют в каждом сценарии.

Полезные ссылки