Как работает кеширование в Next.js v13+ с App Router

Кеширование в Next.js с App Router

Подробно разбираем, как устроено кеширование в Next.js 13+, какие типы кэша бывают и как это влияет на производительность и SEO.

Что такое кэш и как он работает в Next.js

Кэш — это способ сохранить результат вычислений или запроса, чтобы не выполнять его повторно и ускорить работу приложения. В Next.js кэшируются HTML-страницы, данные, запросы, рендеринг компонентов и даже целые маршруты.

С переходом к App Router (начиная с Next.js 13) философия изменилась: теперь кеш включён по умолчанию, а задача разработчика — отключать его там, где нужна динамика.

Таким образом, политика Next.js стала следующей:

"Мы всё кэшируем, если не сказано иное"

Если не указать явно cache: 'no-store', next: { revalidate } или dynamic = 'force-dynamic', то:

  • fetch() будет закеширован
  • layout и page — возможно, тоже
  • маршруты — попадут в кеш маршрутов
  • рендер страницы — в Full Route Cache

Это даёт производительность, но может привести к устаревшим данным и сложностям в отладке.

Типы кэширования в Next.js

Пример использования Next.js

Источник: Next.js Documentation

Full Route Cache (полное кэширование маршрута, сервер)

Используется при ISR (Incremental Static Regeneration).

  • Кэшируется вся страница (HTML, данные из layout.tsx, page.tsx, fetch(), headers(), cookies()).
  • Кэш живёт на сервере.
  • Работает по умолчанию, если нет динамики.
  • Инвалидируется через revalidate или revalidateTag().

Подходит для блогов, маркетинговых страниц, где данные нечасто меняются.

Пример:

export const revalidate = 60; // пересоздавать страницу раз в 60 сек

Data Cache (кэширование fetch-запросов, сервер)

  • Кэшируются ответы fetch() в Server Components.
  • Управляется через next: { revalidate } или cache: 'no-store'.

Пример:

await fetch('https://api.com/posts', {
  next: { revalidate: 300 },
});

Request Memoization (память в пределах одного запроса, сервер)

Это внутренняя оптимизация Next.js: один и тот же fetch() или функция с одинаковыми аргументами вызывается только один раз за один рендер.

  • В пределах одного рендера на сервере Next.js отслеживает одинаковые запросы.
  • Если они уже были вызваны — результат берётся из временного кеша (in-memory).
  • Не зависит от cache: 'force-cache' или no-store — работает всегда.

Подходит для:

  • DRY (don’t repeat yourself) при вызовах одних и тех же данных из разных компонентов.
  • Повышения производительности без ручной оптимизации.
  • Когда один и тот же fetch используется, например, в layout.tsx и page.tsx.

Не подходит для:

  • Многоразового использования между разными запросами или пользователями (это не глобальный кэш).
  • Обхода кэширования fetch() — он работает поверх Request Memoization.
// lib/getUser.ts
export async function getUser() {
  return fetch('https://api.com/user').then(res => res.json());
}

// app/layout.tsx
export default async function Layout({ children }) {
  const user = await getUser(); // Вызов 1
  return (
    <div>
      <Sidebar user={user} />
      {children}
    </div>
  );
}

// app/page.tsx
import { getUser } from '../lib/getUser';

export default async function Page() {
  const user = await getUser(); // Вызов 2, но реально он не повторяется
  return <p>Hello, {user.name}</p>;
}

Физически fetch вызовется один раз, а результат подставится в оба места.

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

Это кэш, который вы контролируете сами через библиотеки, например SWR, React Query и т.п.

  • Работает только при клиентской навигации (<Link>, router.push()).
  • Next.js тут кэш не контролирует — вы сами решаете, как кэшировать.
  • Очень быстрый, потому что ничего не рендерит заново — React просто монтирует из памяти.
  • Но живет только пока пользователь не перезагрузит страницу (F5).

Подходит для интерактивных компонентов, форм, дашбордов.

Пример:

'use client';
import useSWR from 'swr';

const { data } = useSWR('/api/data', fetcher);

Визуальное сравнение типов кэша

Тип кэшаГде работаетУправлениеПример
Full Route CacheСервер (весь route)revalidate, tagexport const revalidate = 60
Data CacheСервер (fetch)cache, revalidatefetch(..., { next: ... })
Static Gen CacheСервер (build time)НетJSX без данных
Client-Side CacheБраузер / клиентВы самиSWR / React Query

Пример из реальной жизни

Допустим, вы делаете магазин:

СтраницаЧто кешироватьКакой тип кэша нужен
/productsСписок товаров, обновляется каждые 5 минутFull Route Cache (ISR)
/api/productsAPI-данные товаровData Cache (через revalidateTag)
Навигация SPAБыстрая навигацияRouter Cache (клиентский кэш)

Частые проблемы с кешем в App Router

Обновил данные — а на сайте старое

fetch() использует force-cache по умолчанию, если не указать cache: 'no-store' или next: { revalidate }. В результате Next.js закешировал данные и никогда не обновляет их.

Решение:

  • Добавьте cache: 'no-store' для отключения кеша
  • Или next: { revalidate: 60 } для ISR
fetch('https://api.com/data', { next: { revalidate: 60 } });

Изменился URL (searchParams), а данные те же

Next.js не перерендеривает fetch-запрос, если URL тот же. Если вы используете параметры, добавляйте их в сам URL fetch:

fetch(`https://api.com/posts?category=${searchParams.category}`, {
  next: { revalidate: 60 }
});

Перешёл на страницу по <Link>, а данные не обновились

Сработал Router Cache (клиентский кэш). Next.js не делает новый запрос при возврате на страницу.

Решение:

  • Принудительно обновляйте данные через router.refresh()
  • Используйте cache: 'no-store', если данные критично свежие

revalidatePath() или revalidateTag() не работает

  • Кэш не создавался (например, из-за no-store)
  • Не используете tags в fetch
  • Вызываете revalidatePath() вне server action

Решение:

  • Убедитесь, что fetch использует next: { tags: [...] }
  • revalidateTag('tag') вызывается в серверной функции

Layout закешировался и не обновляется

layout.tsx тоже кешируется как RSC с revalidate, и если данные внутри закешированы — они не обновятся до следующего ISR.

Решение:

  • Используйте cache: 'no-store' для fetch в layout.tsx, если там, например, профиль пользователя
  • Или уберите fetch из layout и перенесите в page (или client component с SWR)

Непонятное поведение при разработке (dev)

В next dev многие кэши отключены или работают по-другому, revalidate не работает стабильно в dev-режиме.

Рекомендация:
Тестируйте кэш через next build && next start или Vercel preview.

Комбинация cookies(), headers(), params и fetch() ломает кеш

Как только вы используете "динамичные" вещи, вроде:

cookies()
headers()
useSearchParams()
generateMetadata() с dynamic

Next автоматически отключает кэш страницы и переходит в dynamic = 'force-dynamic'.

Чеклист: как избежать проблем с кешем

  • Всегда задавайте поведение fetch явно:
    // нет кеша вообще
    fetch(..., { cache: 'no-store' })
    
    // ISR (обновить каждые 60 сек)
    fetch(..., { next: { revalidate: 60 } })
    
    // ручной сброс через тег
    fetch(..., { next: { tags: ['products'] } })
    
  • Не забывайте включать query-параметры в сам fetch URL
  • Проверяйте next build && next start — иначе можно получить ложную уверенность
  • Если нужно сбрасывать данные вручную — используйте revalidatePath() или revalidateTag()