17. Кэширование
⚡ Кэширование в Next.js: 4 слоя магии
Заголовок раздела «⚡ Кэширование в Next.js: 4 слоя магии»Кэширование — это когда ты однажды что-то вычислил и сохранил результат, чтобы не считать снова. Next.js имеет 4 уровня кэша, каждый со своей ролью. Представь это как матрёшку 🪆: каждый слой быстрее предыдущего. Понять их — значит уметь делать сайты, которые летают!
🗺️ Карта кэшей Next.js
Заголовок раздела «🗺️ Карта кэшей Next.js»Запрос пользователя ↓┌─────────────────────────────────────────┐│ 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) │ ← дедупликация запросов в одном рендере└─────────────────────────────────────────┘1️⃣ Request Memoization: дедупликация
Заголовок раздела «1️⃣ Request Memoization: дедупликация»Самый нижний уровень — автоматическая дедупликация одинаковых fetch() запросов в рамках одного рендера:
// Три разных компонента на одной странице запрашивают одни данные// Next.js делает ОДИН сетевой запрос вместо трёх!
async function UserAvatar({ id }: { id: string }) { const user = await getUser(id); // fetch() #1 return <img src={user.avatar} />;}
// components/UserName.tsxasync function UserName({ id }: { id: string }) { const user = await getUser(id); // fetch() #2 — тот же запрос! return <span>{user.name}</span>;}
// components/UserBio.tsxasync 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-запросы - Только в рамках одного серверного рендера
- Автоматически — ничего настраивать не нужно!
2️⃣ Data Cache: результаты fetch на сервере
Заголовок раздела «2️⃣ Data Cache: результаты fetch на сервере»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'], },});🏷️ Cache Tags: точечная инвалидация
Заголовок раздела «🏷️ Cache Tags: точечная инвалидация»// Теги позволяют инвалидировать только нужные данные
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');}3️⃣ Full Route Cache: HTML-страницы на сервере
Заголовок раздела «3️⃣ Full Route Cache: HTML-страницы на сервере»Full Route Cache хранит скомпилированный HTML и RSC payload для статических маршрутов:
// ✅ Статический маршрут (кэшируется в 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.tsximport { 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; // не кэшировать4️⃣ Router Cache: RSC payload в браузере
Заголовок раздела «4️⃣ Router Cache: RSC payload в браузере»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. Навигация — автоматически обновляет при истечении времени🚫 no-store vs force-cache
Заголовок раздела «🚫 no-store vs force-cache»// ❓ Когда что использовать?
// 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 — для данных с TTLconst posts = await fetch('/api/posts', { next: { revalidate: 60 * 5 }, // 5 минут // Используй когда: посты, товары, новости // Сочетание производительности и свежести данных});
// Глобально для сегмента:// layout.tsx или page.tsxexport 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 Handlerimport { 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: кэш для БД запросов
Заголовок раздела «🏗️ Unstable_cache: кэш для БД запросов»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()