SSR-кэширование в NextJS: ускоряем рендеринг в 10 раз

Разбираем стратегии кэширования Server Components в NextJS 14+. Revalidation, ISR, on-demand revalidation и edge caching.

Дмитрий Мещеряков
Дмитрий Мещеряков
📅 27 июля 2026 г.📖 5 мин чтения

Кэширование в 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 автоматически кэшируется. Настройки через опции:

typescript
// Кэш навсегда (по умолчанию в 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 позволяет обновлять статические страницы без полной пересборки:

typescript
// 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

Когда данные меняются, можно инвалидировать кэш вручную:

typescript
// 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 в переменных окружения. Никогда не коммитьте секреты в репозиторий!

Тегирование запросов

Теги позволяют группировать связанные запросы для инвалидации:

typescript
// Помечаем запрос тегом
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:

typescript
// 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
  • Глобальное распределение

Мониторинг кэша

Добавьте логирование для отладки:

typescript
// lib/cache-logger.ts
export function logCacheStatus(key: string, hit: boolean) {
  if (process.env.NODE_ENV === 'development') {
    console.log(`[Cache] ${key}: ${hit ? 'HIT' : 'MISS'}`);
  }
}

Как определить, какой слой отдаёт старое

Порядок проверки, который экономит больше всего времени. Он идёт снаружи внутрь — от самого дешёвого шага к самому дорогому.

  1. Откройте страницу в приватном окне или обновите принудительно. Если данные стали свежими, виноват Router Cache — клиентский кэш навигации, живущий в памяти вкладки. Он сбрасывается перезагрузкой и с сервером никак не связан.
  2. Запросите страницу через curl без заголовков браузера. Свежий HTML при устаревшем в браузере — снова клиентский слой; устаревший HTML означает проблему на сервере, идём дальше.
  3. Проверьте, что сам источник отдаёт новые данные. Дёрните API напрямую по тому же адресу, который использует fetch. Удивительно часто выясняется, что кэш Next.js ни при чём, а устарел ответ бэкенда или CDN перед ним.
  4. Посмотрите на маршрут в выводе next build. Значок ○ означает статику, ● — SSG с параметрами, ƒ — динамический рендер. Страница, которую вы считали динамической, вполне может оказаться статической и лежать в Full Route Cache до следующей сборки.
  5. И только после этого трогайте revalidate.

Разница в стоимости шагов существенная: первые два занимают минуту, последний — полный цикл сборки и выкладки.

Итоги

  1. По умолчанию — fetch кэшируется навсегда
  2. revalidate — устанавливайте TTL для динамических данных
  3. Теги — группируйте запросы для точечной инвалидации
  4. On-demand — вызывайте из webhooks при изменении данных
  5. Edge — используйте для глобальной CDN-раздачи

Правильное кэширование — это баланс между свежестью данных и производительностью.

Совет «начните с агрессивного кэширования и ослабляйте» стоит уточнить: он верен для контента и неверен для персональных данных. Ошибка в сторону излишнего кэширования на публичной странице означает устаревшую информацию — неприятно, но исправимо. Та же ошибка на странице с данными пользователя означает, что один посетитель увидит данные другого, и это уже инцидент.

Отсюда рабочее правило: начинайте с агрессивного кэширования публичного контента и с полного отказа от кэша для всего, что зависит от пользователя. Ослаблять первое и усиливать второе — по мере того, как разберётесь.

И последнее, что экономит больше всего времени при отладке: определите, какой именно слой отдаёт вам старые данные, прежде чем что-то менять. Кэш данных, кэш маршрута и кэш клиентского роутера дают одинаковый симптом, но лечатся по-разному, а перебор наугад в этой теме особенно затратен.

Частые вопросы

Сколько уровней кэширования в Next.js?

Четыре. Request Memoization дедуплицирует одинаковые запросы в рамках одного рендера. Data Cache хранит результаты fetch между рендерами. Full Route Cache кэширует готовый HTML маршрута. Router Cache живёт в браузере и обслуживает клиентскую навигацию. Все четыре дают одинаковый симптом устаревших данных, но лечатся по-разному.

Что делать, если страница показывает старые данные?

Сначала определить, какой именно слой их отдаёт, и только потом что-то менять. Порядок проверки от дешёвого к дорогому: открыть страницу в приватном окне (если данные свежие — виноват клиентский Router Cache); запросить страницу через curl без заголовков браузера; проверить, что сам источник отдаёт новые данные, дёрнув API напрямую; посмотреть на значок маршрута в выводе next build, потому что страница, которую вы считали динамической, может оказаться статической. И только после этого трогать revalidate.

Как настроить ISR в Next.js?

Экспортом revalidate на уровне страницы плюс generateStaticParams для списка путей. Страница генерируется при первом запросе и кэшируется; по истечении указанного интервала следующий запрос триггерит фоновую регенерацию, а посетитель получает предыдущую версию мгновенно. Это компромисс между статикой и SSR: свежесть данных ограничена интервалом, зато время ответа как у статики.

Как инвалидировать кэш при изменении данных?

Через revalidatePath или revalidateTag в защищённом секретом эндпоинте, который дёргается вебхуком из CMS. Теги удобнее: помечаете запросы в опции next.tags, в том числе несколькими сразу, и потом сбрасываете либо всю группу, либо один объект. Секрет для такого эндпоинта хранят в переменных окружения, а не в репозитории.

Можно ли полагаться на поведение кэширования по умолчанию?

Не стоит. Поведение по умолчанию менялось между мажорными версиями Next.js — в частности, кэшируется ли fetch без явных параметров и как ведут себя маршруты без динамических функций. Задавайте стратегию явно: явный revalidate или явное указание не кэшировать переживут обновление фреймворка, а умолчания — не обязательно.

С какого уровня кэширования начинать?

Совет «начните с агрессивного и ослабляйте» верен для контента и неверен для персональных данных. Ошибка в сторону излишнего кэширования на публичной странице означает устаревшую информацию — неприятно, но исправимо. Та же ошибка на странице с данными пользователя означает, что один посетитель увидит данные другого, и это уже инцидент. Рабочее правило: агрессивно кэшируйте публичный контент и полностью отключайте кэш для всего, что зависит от пользователя.