Кэширование в Next.js — тема, где чаще всего теряют время не из-за сложности механизмов, а из-за непонимания, сколько их. Слоёв несколько: кэш запросов данных, кэш отрендеренных маршрутов, кэш клиентского роутера. Симптом «изменил данные, страница прежняя» может относиться к любому из них, и ищут обычно не в том.
Поведение кэширования по умолчанию менялось между мажорными версиями Next.js — в частности, кэшируется ли fetch без явных параметров и как ведут себя маршруты без динамических функций. Прежде чем полагаться на умолчания, проверьте их в документации своей версии: приёмы из статьи остаются рабочими, а вот значения по умолчанию стоит уточнить.
Практический вывод из этого простой и полезный сам по себе: задавайте стратегию кэширования явно, а не полагайтесь на умолчания. Явный revalidate или явное указание не кэшировать переживут обновление фреймворка; поведение по умолчанию — не обязательно.
Почему кэширование критично для SSR
Server-Side Rendering — это мощно, но дорого. Каждый запрос выполняет рендеринг на сервере: запросы к БД, API, сборка HTML. Без кэширования время ответа растёт линейно с нагрузкой.
NextJS 14+ предлагает несколько уровней кэширования:
- Request Memoization — дедупликация запросов в рамках одного рендера
- Data Cache — кэш fetch-запросов между рендерами
- Full Route Cache — кэш всего HTML-ответа
- Router Cache — клиентский кэш для навигации
Правильная стратегия кэширования может ускорить ваш сайт с 2–3 секунд до 50–100 миллисекунд. Это не оптимизация — это необходимость.
Базовый fetch с кэшированием
В NextJS fetch автоматически кэшируется. Настройки через опции:
// Кэш навсегда (по умолчанию в Server Components)
const data = await fetch('https://api.example.com/data');
// Кэш на 1 час
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }
});
// Без кэша
const data = await fetch('https://api.example.com/data', {
cache: 'no-store'
});ISR — Incremental Static Regeneration
ISR позволяет обновлять статические страницы без полной пересборки:
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // Ревалидация каждый час
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function PostPage({ params }) {
const post = await getPost(params.slug);
return <Article post={post} />;
}Страница генерируется при первом запросе и кэшируется. Через час следующий запрос триггерит фоновую регенерацию.
On-Demand Revalidation
Когда данные меняются, можно инвалидировать кэш вручную:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const { secret, path, tag } = await request.json();
// Проверка секрета
if (secret !== process.env.REVALIDATION_SECRET) {
return Response.json({ error: 'Invalid secret' }, { status: 401 });
}
// Инвалидация по пути
if (path) {
revalidatePath(path);
}
// Инвалидация по тегу
if (tag) {
revalidateTag(tag);
}
return Response.json({ revalidated: true, now: Date.now() });
}Храните REVALIDATION_SECRET в переменных окружения. Никогда не коммитьте секреты в репозиторий!
Тегирование запросов
Теги позволяют группировать связанные запросы для инвалидации:
// Помечаем запрос тегом
const posts = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
});
// Помечаем несколькими тегами
const post = await fetch(`https://api.example.com/posts/${id}`, {
next: { tags: ['posts', `post-${id}`] }
});
// Инвалидируем все посты
revalidateTag('posts');
// Инвалидируем один пост
revalidateTag('post-123');Кэширование на Edge
Для максимальной скорости используйте Edge Runtime:
// app/api/data/route.ts
export const runtime = 'edge';
export async function GET() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
});
return Response.json(await data.json(), {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300'
}
});
}Edge-функции работают ближе к пользователю (CDN), что даёт:
- Меньшую задержку (latency)
- Быстрый cold start
- Глобальное распределение
Мониторинг кэша
Добавьте логирование для отладки:
// lib/cache-logger.ts
export function logCacheStatus(key: string, hit: boolean) {
if (process.env.NODE_ENV === 'development') {
console.log(`[Cache] ${key}: ${hit ? 'HIT' : 'MISS'}`);
}
}Итоги
- По умолчанию — fetch кэшируется навсегда
- revalidate — устанавливайте TTL для динамических данных
- Теги — группируйте запросы для точечной инвалидации
- On-demand — вызывайте из webhooks при изменении данных
- Edge — используйте для глобальной CDN-раздачи
Правильное кэширование — это баланс между свежестью данных и производительностью.
Совет «начните с агрессивного кэширования и ослабляйте» стоит уточнить: он верен для контента и неверен для персональных данных. Ошибка в сторону излишнего кэширования на публичной странице означает устаревшую информацию — неприятно, но исправимо. Та же ошибка на странице с данными пользователя означает, что один посетитель увидит данные другого, и это уже инцидент.
Отсюда рабочее правило: начинайте с агрессивного кэширования публичного контента и с полного отказа от кэша для всего, что зависит от пользователя. Ослаблять первое и усиливать второе — по мере того, как разберётесь.
И последнее, что экономит больше всего времени при отладке: определите, какой именно слой отдаёт вам старые данные, прежде чем что-то менять. Кэш данных, кэш маршрута и кэш клиентского роутера дают одинаковый симптом, но лечатся по-разному, а перебор наугад в этой теме особенно затратен.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.