Как работает кеширование в Next.js v13+ с 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
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 , tag | export const revalidate = 60 |
Data Cache | Сервер (fetch) | cache , revalidate | fetch(..., { next: ... }) |
Static Gen Cache | Сервер (build time) | Нет | JSX без данных |
Client-Side Cache | Браузер / клиент | Вы сами | SWR / React Query |
Пример из реальной жизни
Допустим, вы делаете магазин:
Страница | Что кешировать | Какой тип кэша нужен |
---|---|---|
/products | Список товаров, обновляется каждые 5 минут | Full Route Cache (ISR) |
/api/products | API-данные товаров | 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()