Перейти к содержимому

17. Кэширование

Кэширование — это когда ты однажды что-то вычислил и сохранил результат, чтобы не считать снова. Next.js имеет 4 уровня кэша, каждый со своей ролью. Представь это как матрёшку 🪆: каждый слой быстрее предыдущего. Понять их — значит уметь делать сайты, которые летают!


Запрос пользователя
┌─────────────────────────────────────────┐
│ 4. Router Cache (client-side) │ ← RSC payload в памяти браузера
├─────────────────────────────────────────┤
│ 3. Full Route Cache (server-side) │ ← HTML + RSC payload на сервере
├─────────────────────────────────────────┤
│ 2. Data Cache (server-side) │ ← результаты fetch() и db-запросов
├─────────────────────────────────────────┤
│ 1. Request Memoization (per-request) │ ← дедупликация запросов в одном рендере
└─────────────────────────────────────────┘

Самый нижний уровень — автоматическая дедупликация одинаковых fetch() запросов в рамках одного рендера:

components/UserAvatar.tsx
// Три разных компонента на одной странице запрашивают одни данные
// Next.js делает ОДИН сетевой запрос вместо трёх!
async function UserAvatar({ id }: { id: string }) {
const user = await getUser(id); // fetch() #1
return <img src={user.avatar} />;
}
// components/UserName.tsx
async function UserName({ id }: { id: string }) {
const user = await getUser(id); // fetch() #2 — тот же запрос!
return <span>{user.name}</span>;
}
// components/UserBio.tsx
async function UserBio({ id }: { id: string }) {
const user = await getUser(id); // fetch() #3 — снова тот же!
return <p>{user.bio}</p>;
}
// Функция с fetch() — автоматически мемоизируется
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
}
// React.cache() для функций БЕЗ fetch:
import { cache } from 'react';
const getUserFromDB = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});

Ключевые факты:

  • Работает только с fetch() (не с Prisma/db напрямую!)
  • Только GET-запросы
  • Только в рамках одного серверного рендера
  • Автоматически — ничего настраивать не нужно!

Data Cache хранит результаты fetch() между запросами пользователей и деплоями. Настраивается через опции fetch:

// По умолчанию: force-cache (Next.js 14) / no-store (Next.js 15)
const data = await fetch('https://api.example.com/data');
// ✅ КЭШИРОВАТЬ навсегда (до ручной инвалидации)
const staticData = await fetch('https://api.example.com/config', {
cache: 'force-cache',
});
// ✅ НЕ кэшировать (каждый запрос — новые данные)
const dynamicData = await fetch('https://api.example.com/prices', {
cache: 'no-store',
});
// ✅ Revalidate через время (ISR-стиль)
const revalidatedData = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }, // обновлять раз в час
});
// ✅ Теги для ручной инвалидации
const taggedData = await fetch('https://api.example.com/products', {
next: {
revalidate: 86400, // 24 часа
tags: ['products', 'catalog'],
},
});

lib/data.ts
// Теги позволяют инвалидировать только нужные данные
export async function getProducts() {
return fetch('https://api.example.com/products', {
next: { tags: ['products'] }
}).then(r => r.json());
}
export async function getProduct(id: string) {
return fetch(`https://api.example.com/products/${id}`, {
next: { tags: ['products', `product-${id}`] }
}).then(r => r.json());
}
// app/actions/products.ts
'use server';
import { revalidateTag, revalidatePath } from 'next/cache';
export async function updateProduct(id: string, data: FormData) {
await db.product.update({ where: { id }, data: Object.fromEntries(data) });
// Инвалидируем только данные этого продукта
revalidateTag(`product-${id}`);
// Инвалидируем весь список продуктов
revalidateTag('products');
// Инвалидируем конкретные страницы
revalidatePath(`/products/${id}`);
revalidatePath('/products');
}

Full Route Cache хранит скомпилированный HTML и RSC payload для статических маршрутов:

app/blog/page.tsx
// ✅ Статический маршрут (кэшируется в Full Route Cache)
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }, // обновляется раз в час
}).then(r => r.json());
return <PostList posts={posts} />;
}
// ❌ Динамический маршрут (НЕ кэшируется)
// Причины:
// • cookies() / headers() в компоненте
// • cache: 'no-store' в fetch
// • export const dynamic = 'force-dynamic'
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
export default async function DashboardPage() {
const cookieStore = await cookies(); // ← делает маршрут динамическим!
const userId = cookieStore.get('user-id')?.value;
// ...
}
// Явно управляем режимом
export const dynamic = 'auto'; // по умолчанию
export const dynamic = 'force-dynamic'; // всегда динамический
export const dynamic = 'force-static'; // всегда статический (ошибка если нельзя)
export const dynamic = 'error'; // ошибка если становится динамическим
// Время жизни кэша
export const revalidate = 3600; // в секундах (ISR)
export const revalidate = false; // никогда не обновлять
export const revalidate = 0; // не кэшировать

Router Cache — клиентский кэш RSC payload (React Server Component payload). Браузер хранит уже загруженные страницы:

Пользователь переходит на /about
→ Браузер запрашивает RSC payload
→ Сохраняет в Router Cache
Пользователь нажимает "Назад"
→ Браузер берёт /about из Router Cache (мгновенно!)
→ Не делает сетевой запрос ✨

Время жизни Router Cache:

  • Статические маршруты: 5 минут
  • Динамические маршруты: 30 секунд
  • Prefetch: до 5 минут

Инвалидация Router Cache:

// 1. router.refresh() — перезагружает текущий сегмент
'use client';
import { useRouter } from 'next/navigation';
function RefreshButton() {
const router = useRouter();
return (
<button onClick={() => router.refresh()}>
🔄 Обновить данные
</button>
);
}
// 2. Server Action с revalidatePath — инвалидирует и сервер, и клиент
'use server';
import { revalidatePath } from 'next/cache';
async function createPost() {
await db.post.create({ data: { title: '...' } });
revalidatePath('/blog'); // инвалидирует Full Route Cache + Router Cache
}
// 3. Навигация — автоматически обновляет при истечении времени

// ❓ Когда что использовать?
// force-cache — для неизменяемых данных
const config = await fetch('/api/config', {
cache: 'force-cache',
// Используй когда: конфиги, справочники, переведённые строки
// Инвалидируй: при деплое или revalidateTag()
});
// no-store — для персональных/real-time данных
const cart = await fetch('/api/cart', {
cache: 'no-store',
// Используй когда: корзина, уведомления, real-time данные
// Делает маршрут ДИНАМИЧЕСКИМ
});
// revalidate — для данных с TTL
const posts = await fetch('/api/posts', {
next: { revalidate: 60 * 5 }, // 5 минут
// Используй когда: посты, товары, новости
// Сочетание производительности и свежести данных
});
// Глобально для сегмента:
// layout.tsx или page.tsx
export const fetchCache = 'default-cache'; // кэшировать по умолчанию
export const fetchCache = 'force-cache'; // всегда кэш
export const fetchCache = 'force-no-store'; // никогда кэш

// 1. Логирование (dev режим)
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
});
// В консоли Next.js dev сервера:
// fetch /api/data (cache: HIT) ← из кэша
// fetch /api/data (cache: MISS) ← сетевой запрос
// 2. Заголовки ответа
// x-nextjs-cache: HIT | MISS | STALE | REVALIDATED
// 3. Принудительная очистка в dev
// rm -rf .next/cache
// 4. Переменные окружения
NEXT_PRIVATE_DEBUG_CACHE=1 npm run dev
// 5. Проверка через headers в Route Handler
import { headers } from 'next/headers';
export async function GET() {
const headersList = await headers();
const cacheControl = headersList.get('cache-control');
console.log('Cache-Control:', cacheControl);
return Response.json({ ok: true });
}

unstable_cache позволяет кэшировать любые асинхронные операции (не только fetch):

import { unstable_cache } from 'next/cache';
// Кэшируем Prisma запрос (работает как Data Cache для fetch)
const getUsers = unstable_cache(
async () => {
return db.user.findMany({
select: { id: true, name: true, email: true },
});
},
['users-list'], // ключ кэша
{
revalidate: 3600, // TTL в секундах
tags: ['users'], // теги для инвалидации
}
);
// Использование
export default async function UsersPage() {
const users = await getUsers(); // будет кэшироваться!
return <UserList users={users} />;
}

Данные меняются раз в год? → force-cache (или generateStaticParams)
Данные меняются раз в час? → revalidate: 3600
Данные меняются раз в минуту? → revalidate: 60
Данные персональные? → no-store (динамический рендер)
Данные real-time? → no-store + WebSocket/SSE
Данные часто, но инвалидируем? → тэги + revalidateTag()