open atlas
↑ К треку
Next.js с нуля до senior NEXT · 02 · 02

Четыре кеша Next.js — и почему ваши данные не обновляются

Четыре кеша: Request Memoization (один рендер), Data Cache (сервер, переживает деплои), Full Route Cache (HTML+RSC, сброс деплоем), Router Cache (клиент). Протухшие данные диагностируют послойно; revalidatePath/Tag инвалидируют; в Next 15 fetch по умолчанию не кешируется.

NEXT Senior ◷ 20 min
Уровень
ОсновыJuniorMiddleSenior

Контент-команда публикует изменение цены в CMS в пятницу днём. Дежурный инженер проверяет прод: старая цена. Он передеплоивает — всё ещё старая цена. Теперь это официально жутко: свежий деплой отдаёт данные, которых больше нигде не существует. Он добавляет в fetch cache: 'no-store', деплоит ещё раз — страница обновляется, но теперь каждый запрос бьёт в CMS, и p95-латентность утраивается. Постмортем в понедельник находит в цепочке три отдельных кеша, у каждого свой срок жизни: Data Cache хранил ответ CMS персистентно — он по дизайну переживает деплои, Full Route Cache запёк протухшие данные в статический HTML на этапе сборки, а часть пользователей видела старую цену даже после починки сервера, потому что Router Cache их браузера держал предыдущий payload. Каждый «фикс» атаковал один слой, пока другой продолжал отдавать протухшие байты. Кеширование в Next.js — это не один кеш, который надо сбросить; это четыре кеша, поставленные друг на друга, с разными хранилищами, областями действия и правилами инвалидации — и отлаживают их по порядку.

Четыре слоя: от самого внутреннего к ближайшему к пользователю

После этого раздела вы сможете называть ответственный кеш по симптому протухших данных и выбирать нужный рычаг инвалидации вместо того, чтобы тянуться за no-store на всё подряд.

Request Memoization (мемоизация запросов) — это React, а не инфраструктура: одинаковые вызовы fetch (тот же URL, те же опции, GET) в рамках одного серверного прохода рендера выполняются один раз; остальные получают мемоизированный результат. Срок жизни: один запрос — таблица мемоизации умирает вместе с окончанием рендера. Поэтому layout и страница могут оба вызвать getUser() без двойного похода в базу, и поэтому пропсы из layout в page не протаскивают. Мемоизация дедуплицирует только внутри одного рендера; второму запросу она не отдаёт ничего. Для не-fetch работы (прямой запрос в БД) ту же пер-запросную мемоизацию даёт React.cache().

Data Cache — серверное хранилище ответов fetch, с ключом URL плюс опции. Его определяющее — и самое неожиданное — свойство: он персистентен между входящими запросами и между деплоями. Выкат нового кода его не очищает; пятничный баг с ценой живёт именно здесь. Управление — на каждый fetch: next: { revalidate: 3600 } для протухания по времени, next: { tags: ['products'] } для точечной очистки через revalidateTag, cache: 'no-store' — полный обход. В Next 14 и раньше ответы fetch кешировались здесь по умолчанию (force-cache); в Next 15 умолчание перевернулось на «не кешировать» — об этом ниже.

Full Route Cache — отрендеренный результат статически рендеримых маршрутов: RSC-payload плюс HTML, произведённые на этапе сборки или в фоне после ревалидации. Именно он позволяет статическому маршруту отвечать за единицы миллисекунд, не выполняя ваши компоненты. Область: сервер; срок жизни: до ревалидации, и, в отличие от Data Cache, он сбрасывается каждым деплоем. Маршруты, использующие динамические API — cookies(), headers(), чтение searchParams в странице или dynamic = 'force-dynamic', — выходят из него целиком и рендерятся на каждый запрос.

Router Cache живёт в памяти браузера: он хранит RSC-payload посещённых сегментов на время сессии, поэтому навигация назад/вперёд мгновенна, а префетченные ссылки открываются без похода на сервер. Это единственный клиентский слой — и единственный, который ваш сервер не может сбросить напрямую: он очищается жёсткой перезагрузкой, router.refresh() или когда серверный экшен вызывает revalidatePath/revalidateTag либо ставит cookie.

// Один fetch, три решения о кешировании:
const res = await fetch('https://cms.example.com/api/products/42', {
  next: {
    revalidate: 3600,        // Data Cache: протухает через 1ч, обновляется в фоне
    tags: ['products', 'product-42'], // Data Cache: можно сбросить по требованию
  },
});

// В серверном экшене, редактирующем товар:
import { revalidateTag } from 'next/cache';

export async function updateProduct(formData: FormData) {
  'use server';
  await db.product.update(/* ... */);
  revalidateTag('product-42'); // очищает записи Data Cache → Full Route Cache
                               // перегенерируется → клиентский Router Cache сбрасывается
}

Компромисс: каждый слой меняет свежесть на свою цену. Мемоизация — бесплатная корректность. Data Cache экономит нагрузку на источник, но может отдавать данные, которых больше не существует, — через деплои. Full Route Cache даёт латентность уровня CDN, но только маршрутам, отказавшимся от данных на каждый запрос. Router Cache делает навигацию нативной по ощущению — но это значит, что сервер уже починен, а вкладка пользователя всё ещё показывает баг.

Инвалидация: revalidate, теги и выход из кеша

Ревалидация по времени (revalidate: 3600 или export const revalidate = 3600 на уровне сегмента) работает как stale-while-revalidate: первый запрос после истечения всё ещё получает протухшую запись, перезапрос происходит в фоне, и свежие данные видит уже следующий запрос. Этот «ещё один протухший хит» удивляет тех, кто ждал жёсткого TTL.

Инвалидация по требованию — точный инструмент: revalidateTag('products') вычищает каждую запись Data Cache с этим тегом и инвалидирует Full Route Cache маршрутов, собранных из неё; revalidatePath('/products') делает то же по маршруту. Вызванные внутри серверного экшена, они инвалидируют и клиентский Router Cache затронутых путей — пользователь, сделавший мутацию, видит результат сразу. Вызванные из route handler (скажем, вебхук CMS — правильный фикс пятничного бага), они чинят серверные кеши, но до браузеров дотянуться не могут; открытые вкладки догонят при следующей жёсткой навигации или перезагрузке.

Выход из кеша — по слоям: cache: 'no-store' пропускает Data Cache для одного fetch; export const dynamic = 'force-dynamic' заставляет весь маршрут рендериться на каждый запрос (мимо Full Route Cache); cookies() или headers() делают то же неявно. Заметьте асимметрию: некешируемый fetch внутри в остальном статического маршрута делает маршрут динамическим — но динамичность маршрута не мешает его fetch-ам пользоваться Data Cache. Кеширование маршрута и кеширование данных — независимые оси.

Викторина

На Next 14 маркетинговая страница запрашивает цены из CMS обычным fetch (без опций). Контент в CMS меняется, команда передеплоивает сайт — а прод всё ещё отдаёт старую цену. Почему свежий деплой может отдавать протухшие данные?

Диагностика «мои данные не обновляются» — слой за слоем

Это самый частый production-баг Next.js, и он поддаётся упорядоченному обходу — сначала сервер, потом клиент.

Шаг 1 — маршрут статический? Смотрите вывод сборки: кружок — статический, ƒ — динамический. Если он статический, а данные нужны на каждый запрос, Full Route Cache отдаёт HTML времён сборки, и никакая опция fetch не спасёт — маршруту нужен dynamic = 'force-dynamic' или динамический API. Если он должен быть статическим, но периодически свежим — задайте revalidate.

Шаг 2 — fetch кешируется? На Next 14 голый fetch по умолчанию force-cache: вашей записи может быть несколько месяцев, и деплой её не берёт. Определите реальный контракт свежести: окно revalidate, теги плюс revalidateTag из вебхука или no-store, если данные действительно на каждый запрос.

Шаг 3 — мутация инвалидирует? Серверный экшен, который пишет в базу, но не вызывает revalidatePath/revalidateTag, оставляет все кеши нетронутыми — запись прошла, просто кешам не сказали. Это самое частое упущение.

Шаг 4 — протухло только в одной вкладке? Сервер по curl отвечает свежим, а пользователь видит старые данные при навигации назад: это Router Cache. Если протухание следует за мутацией — фикс относится к шагу 3 (экшены инвалидируют и Router Cache); для свежести «данные изменились где-то ещё» варианты — router.refresh() или осознанно принятая сессионная протухлость.

Вместе эти четыре шага означают, что виновник всегда известен до того, как вы трогаете код: протухает после деплоя — Data Cache; только одна вкладка — Router Cache; кружок в выводе сборки — Full Route Cache. Без шага 1 можно потратить час на добавление no-store к fetch внутри статически рендеримого маршрута, где fetch на каждый запрос вообще не выполняется.

Дисциплина в том, чтобы назвать слой до того, как тянуться за фиксом. Рассыпанный везде no-store «работает», схлопывая все четыре слоя, — и утраивает нагрузку на источник; именно так пятничный хотфикс уронил p95.

Next 15 перевернул умолчания — версионируйте свои советы

Всё выше — механика, общая для обоих мажоров, но умолчания в Next 15 изменились существенно, и половина советов в интернете молча предполагает не те. В Next 15: fetch больше не кешируется по умолчанию (семантика no-store, пока не включите явно через cache: 'force-cache' или опцию revalidate), GET route handlers больше не кешируются по умолчанию, и Router Cache больше не переиспользует сегменты страниц по умолчанию (staleTime 0 для страниц — свежая навигация перезапрашивает страницу, тогда как восстановление назад/вперёд и layout по-прежнему берутся из кеша; настраивается конфигом staleTimes). Full Route Cache и статический рендеринг остаются умолчанием для маршрутов без динамических API.

Практически: на кодовой базе Next 15 пятничный баг в рассказанном виде с голым fetch почти невозможен — но он возвращается, как только кто-то добавляет force-cache ради производительности и забывает инвалидационную половину сделки. А команды, апгрейдящиеся с 14, видят зеркальный инцидент: трафик на источник подскакивает после апгрейда, потому что каждый прежде кешируемый по умолчанию fetch теперь бьёт в источник на каждый запрос. Прежде чем отлаживать любое кеш-поведение, выполните next --version — один и тот же код имеет разную семантику по разные стороны этой границы.

Викторина

Пользователь правит элемент через серверный экшен: запись в БД плюс revalidatePath. Он видит свежие данные. Коллега, у которого страница списка уже была открыта в другом браузере, после навигации туда-обратно в своей вкладке всё ещё видит старый элемент. Какой слой и почему?

Вспомните перед уходом
  1. 01
    Назовите четыре кеша: хранилище, область, срок жизни и способ инвалидации каждого.
  2. 02
    Пройдите диагностику «данные не обновляются» по порядку и скажите, что изменилось в Next 15.
Итог

Next.js ставит друг на друга четыре кеша с разными хранилищами, областями и сроками жизни, и баги протухших данных диагностируются называнием слоя до фикса. Request Memoization дедуплицирует одинаковые fetch внутри одного серверного прохода рендера — поэтому layout и страницы свободно запрашивают одни данные — и умирает с запросом. Data Cache хранит ответы fetch на сервере с ключом URL и опций и персистентен между запросами и деплоями — тот самый кеш, из-за которого свежедеплоенный сайт отдаёт данные, которых больше не существует; управляется на каждый fetch окнами revalidate (stale-while-revalidate: ещё один протухший хит после истечения), тегами для точечной очистки или no-store. Full Route Cache — RSC-payload и HTML статически рендеримых маршрутов времён сборки (или ревалидации) — сбрасывается деплоем, пропускается любым маршрутом с cookies(), headers() или force-dynamic; кеширование маршрута и кеширование данных — независимые оси. Router Cache — in-memory хранилище браузера для посещённых сегментов, делающее назад/вперёд мгновенным, — единственный слой, который сервер не может сбросить удалённо: серверные экшены с revalidatePath/revalidateTag (или установкой cookie) инвалидируют его для мутировавшего пользователя, а чужие открытые вкладки остаются протухшими до перезагрузки. Production-диагностика идёт от сервера к клиенту: статический ли маршрут в выводе сборки, кешируется ли fetch и по какому контракту, вызывает ли мутация revalidatePath/revalidateTag (самое частое упущение) — и лишь затем не Router Cache ли это одного браузера. Ковровый no-store схлопывает все слои и умножает нагрузку на источник — хотфикс, утраивающий p95. Next 15 перевернул умолчания: fetch и GET route handlers не кешируются без явного включения, а Router Cache перестал переиспользовать сегменты страниц (staleTime 0, настраивается через staleTimes), так что один и тот же код имеет разную кеш-семантику по разные стороны границы 14/15 — проверяйте версию до отладки и ждите, что у апгрейднутых приложений подскочит трафик на источник там, где их молча выручало кеширование по умолчанию. Теперь, когда встретишь протухшие данные в проде, — называй слой первым: протухает через деплои — Data Cache, протухает только в одной вкладке — Router Cache, кружок в выводе сборки — Full Route Cache; каждый слой имеет ровно один правильный рычаг.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.