惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

K
Kaspersky official blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
AI
AI
SecWiki News
SecWiki News
宝玉的分享
宝玉的分享
Scott Helme
Scott Helme
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Engineering at Meta
Engineering at Meta
博客园 - 叶小钗
The GitHub Blog
The GitHub Blog
Microsoft Azure Blog
Microsoft Azure Blog
N
News and Events Feed by Topic
Cloudbric
Cloudbric
B
Blog
Cisco Talos Blog
Cisco Talos Blog
V
Vulnerabilities – Threatpost
N
News and Events Feed by Topic
V
Visual Studio Blog
A
Arctic Wolf
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
U
Unit 42
S
Security @ Cisco Blogs
博客园 - 聂微东
T
Threat Research - Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
Y
Y Combinator Blog
G
GRAHAM CLULEY
L
LINUX DO - 热门话题
量子位
NISL@THU
NISL@THU
Webroot Blog
Webroot Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Troy Hunt's Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tenable Blog
月光博客
月光博客
S
Security Affairs
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
The Hacker News
The Hacker News
Spread Privacy
Spread Privacy
D
Docker
www.infosecurity-magazine.com
www.infosecurity-magazine.com
雷峰网
雷峰网
博客园 - 司徒正美
T
The Exploit Database - CXSecurity.com
Hugging Face - Blog
Hugging Face - Blog
Help Net Security
Help Net Security
D
DataBreaches.Net

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Кэширование в Next.js App Router, как увидеть, почему данные не обновились
lemon_m · 2026-04-30 · via Все публикации подряд на Хабре

Время на прочтение9 мин

Охват и читатели0

Туториал

С кэшированием в Next.js обычно случается одна и та же история. API уже отдаёт новые данные, страница открывается заново, а на экране всё ещё старая версия. После этого в код быстро добавляют cache: "no-store", данные начинают запрашиваться на каждый заход, и через пару минут появляется уже другой вопрос - зачем тогда вообще нужен встроенный механизм кэширования.

Проблема в том, что кэширование обычно звучит как одно явление, а на практике в App Router похожие ощущения могут давать разные уровни поведения. Навигация назад и вперёд может переиспользовать клиентский кэш маршрута, сам маршрут может рендериться по-разному, а серверный fetch в Next.js имеет собственные стратегии кэширования и перевалидации. В актуальной документации это уже разделено на новый режим с Cache Components и на прежнюю модель без них. В этой статье речь именно о привычной модели App Router без Cache Components, где поведение обычно задаётся через fetch, cache, next.revalidate и route segment config. (Next.js)

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

Для примеров ниже используется проект Goods Finder и внешний API DummyJSON. Идея - сначала добавить на страницу штамп серверного рендера, потом отдельно показать момент получения данных, а уже после этого сравнить force-cache, no-store и revalidate.

Сначала нужно увидеть проблему

В App Router маршруты по умолчанию рендерятся на сервере, а навигация остаётся client-side. Shared layouts сохраняются, а back/forward могут переиспользовать уже посещённые страницы из client cache. Из-за этого внешне похожие ситуации на самом деле оказываются разными, где-то страница даже не пересчитывалась, а где-то страница пересчиталась, но данные пришли из кэша. (Next.js)

Самый короткий способ добавить на страницу серверный штамп времени.

// src/app/_ui/RenderStamp.js
export default function RenderStamp({ label = "renderedAt" }) {
  const renderedAt = new Date().toISOString();

  return (
    <p className="text-xs text-slate-500">
      {label}: <code className="font-mono">{renderedAt}</code>
    </p>
  );
}

Этот компонент серверный. Значение вычисляется во время серверного рендера. Если страница реально пересчиталась, время изменится. Если был показан повторный результат, штамп останется прежним.

Дальше его удобно подключить на список и на детальную страницу товара.

// src/app/(app)/goods/page.js
import RenderStamp from "@/app/_ui/RenderStamp";

// ... JSX страницы
<RenderStamp label="/goods render" />
// src/app/(app)/goods/[id]/page.js
import RenderStamp from "@/app/_ui/RenderStamp";

// ... JSX страницы
<RenderStamp label="goods/[id] render" />

После этого можно пройти обычный сценарий - открыть /goods, зайти в карточку товара, нажать Back, снова открыть карточку, обновить страницу. На навигации назад штамп не меняется. На reload он меняется. Уже на этом шаге становится видно, что не каждый повторный показ страницы означает новый серверный проход. То, что pages могут переиспользоваться при back/forward navigation, отдельно зафиксировано и в текущем glossary Next.js. (Next.js)

Один рендер и одни данные это не одно и то же

Следующий шаг полезнее любого объяснения про Data Cache. Нужно показать не только момент рендера, но и момент, когда пришли сами данные. Для этого в слой данных можно добавить ещё один маркер. В проекте он берётся из HTTP-заголовка Date.

// src/app/_data/dummyjson.js
const API_BASE = "https://dummyjson.com";

async function fetchJson(url, fetchOptions = {}) {
  const res = await fetch(url, fetchOptions);

  if (!res.ok) {
    const text = await res.text().catch(() => "");
    const err = new Error(`DummyJSON error: ${res.status} ${res.statusText}. ${text}`);
    err.status = res.status;
    throw err;
  }

  const fetchedAt = res.headers.get("date") || new Date().toUTCString();
  const data = await res.json();

  data.__fetchedAt = fetchedAt;

  return data;
}

Теперь можно вернуть этот маркер на страницу рядом с RenderStamp.

// src/app/(app)/goods/[id]/page.js
<p className="text-xs text-slate-500">
  data fetched: <code className="font-mono">{item.__fetchedAt}</code>
</p>
<RenderStamp label="goods/[id] render" />

В этот момент кэширование перестаёт быть абстракцией. Есть два независимых сигнала. Первый показывает, пересчитывалась ли страница. Второй показывает, пришёл ли новый ответ от внешнего API.

Здесь становится видно главное, страница может пересчитаться заново, а данные при этом остаться прежними.

force-cache, рендер может меняться, а данные нет

Теперь можно включить на запросах force-cache.

// src/app/_data/dummyjson.js
export async function getProducts({ q = "", limit = 12, skip = 0 } = {}) {
  const safeQ = String(q).trim();
  const qs = new URLSearchParams({
    limit: String(limit),
    skip: String(skip),
  });

  const url = safeQ
    ? `${API_BASE}/products/search?${qs.toString()}&q=${encodeURIComponent(safeQ)}`
    : `${API_BASE}/products?${qs.toString()}`;

  return fetchJson(url, { cache: "force-cache" });
}

export async function getProductById(id) {
  const safeId = encodeURIComponent(String(id));
  const url = `${API_BASE}/products/${safeId}`;

  return fetchJson(url, { cache: "force-cache" });
}

В актуальной документации force-cache описан буквально так - Next.js ищет совпадение в серверном кэше, если запись свежая, она возвращается из кэша, а если записи нет или она устарела, Next.js идёт в источник и обновляет кэш. (Next.js)

После npm run build && npm start можно открыть, например, /goods/1 и сделать несколько обычных обновлений страницы. Поведение выглядит показательно: goods/[id] render меняется, а data fetched может оставаться прежним. Страница пересчиталась, но данные были переиспользованы из кэша.

Это и есть та ловушка, из-за которой возникает ощущение данные залипли. На самом деле рендер живой, но источник данных не был запрошен заново.

no-store, теперь оба маркера идут вместе

Дальше достаточно заменить режим запроса:

// src/app/_data/dummyjson.js
return fetchJson(url, { cache: "no-store" });

Документация определяет no-store прямо: Next.js запрашивает ресурс у удалённого сервера на каждый запрос, даже если request-time APIs на маршруте не использовались. 

После пересборки и нескольких обновлений страницы уже видно другое поведение. goods/[id] render меняется, и data fetched тоже меняется на каждом обновлении. Здесь рендер и данные идут вместе, потому что сетевой ответ каждый раз новый.

С практической точки зрения это удобно для баланса, статусов заказа, личных кабинетов, внутренних панелей и любых данных, где устаревшая версия уже считается ошибкой. Но такой режим не всегда нужен для публичного каталога. Он увеличивает число запросов и нагружает внешний API там, где обычно хватает более долгой стратегии.

revalidate, не всегда свежо, но предсказуемо

Для каталога, витрины и списков товаров чаще нужен промежуточный режим. Не постоянный сетевой запрос и не бесконечное переиспользование кэша, а окно свежести. В Next.js это делается через next.revalidate.

// src/app/_data/dummyjson.js
const REVALIDATE_SECONDS = 15;

export async function getProductById(id) {
  const safeId = encodeURIComponent(String(id));
  const url = `${API_BASE}/products/${safeId}`;

  return fetchJson(url, { next: { revalidate: REVALIDATE_SECONDS } });
}

В официальной документации next.revalidate задаёт lifetime ресурса в секундах. 0 отключает кэширование, положительное число задаёт максимальное окно жизни записи, а false семантически эквивалентен бесконечному хранению. Чтобы этот режим было легче наблюдать, полезно добавить ещё один маркер freshUntil.

// внутри fetchJson
const revalidateSec = fetchOptions?.next?.revalidate;
const fetchedAtMs = Date.parse(fetchedAt);

const freshUntil =
  Number.isFinite(fetchedAtMs) && Number.isFinite(revalidateSec)
    ? new Date(fetchedAtMs + revalidateSec * 1000).toUTCString()
    : "";

const data = await res.json();
data.__fetchedAt = fetchedAt;
data.__revalidateSec = Number.isFinite(revalidateSec) ? revalidateSec : null;
data.__freshUntil = freshUntil;

И вывести это рядом с рендер-штампом.

<p className="text-xs text-slate-500">
  data fetched: <span className="font-mono">{item.__fetchedAt}</span>
  {item.__revalidateSec ? (
    <>
      <span className="text-slate-300"> • </span>
      revalidate: <span className="font-mono">{item.__revalidateSec}s</span>
      <span className="text-slate-300"> • </span>
      fresh until: <span className="font-mono">{item.__freshUntil}</span>
    </>
  ) : null}
</p>
<RenderStamp label="goods/[id] render" />

Пока запросы укладываются в окно свежести, render может обновляться, а data fetched остаётся прежним. После выхода за окно свежести кэш начинает обновляться, и data fetched меняется. В документации это и описано как time-based revalidation через fetch(..., { next: { revalidate: N } }). Там же отдельно отмечено, что route-level revalidate не переопределяет более частую revalidation у отдельных fetch внутри маршрута.

Как увидеть именно Data Cache, а не всё сразу

На практике у новичка здесь часто ломается наблюдение. Он добавляет revalidate, обновляет страницу и не понимает, какой именно слой сейчас сработал. Чтобы эксперимент был чище, в проекте есть полезный приём - отключить Full Route Cache для сегмента, но оставить Data Cache у fetch. Это можно сделать через request-time API headers().

// src/app/(app)/goods/[id]/page.js
import { headers } from "next/headers";

export default async function GoodsDetailsPage({ params }) {
  const p = await params;
  const item = await getProductById(p.id);

  headers();

  return (
    <div>
      <h1>{item.title}</h1>
      <RenderStamp label="goods/[id] render" />
    </div>
  );
}

В текущей документации headers() описан как Request-time API. Его использование переводит маршрут в dynamic rendering. В guide по прежней модели кэширования также зафиксировано, что dynamic = "force-dynamic" эквивалентен режиму, где все fetch внутри страницы ведут себя как cache: 'no-store' и revalidate: 0. В той же guide отмечено, что по умолчанию fetch до request-time APIs может кэшироваться, а после них нет. (Next.js)

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

Почему в dev всё ощущается иначе

В development поведение действительно отличается. В актуальной документации у fetch прямо сказано, что режим по умолчанию auto no cache в dev запрашивает ресурс на каждый запрос, но во время next build статически подготавливаемый маршрут может получить этот ресурс один раз. Там же указано, что в local development у Server Components есть HMR cache, который по умолчанию распространяется даже на cache: 'no-store'. Из-за этого uncached requests могут не показывать свежие данные между HMR refreshes. Этот кэш очищается при навигации или полном reload. Дополнительно hard refresh и отключённый cache в DevTools часто посылают cache-control: no-cache, и тогда cache, revalidate и tags могут быть проигнорированы, а запрос пойдёт прямо в источник.

Именно поэтому кэширование в Next.js лучше проверять не только в next dev, а обязательно через npm run build && npm start. Для этой темы production-режим не просто финальная проверка, а часть самой диагностики.

Cache-lab как нормальный способ проверить режимы

Чтобы не переключать код вручную между force-cache, no-store и revalidate, в Goods Finder была вынесена отдельная лаборатория /cache-lab. Режим кэша передаётся через query-параметры, слой данных принимает профиль кэша вторым аргументом, а список и карточка товара показывают рядом render, data fetched, revalidate и fresh until.

// src/app/_data/dummyjson.js
function toCacheOptions(profile) {
  const mode = profile?.mode || "force-cache";
  const n = Number(profile?.n);

  if (mode === "no-store") return { cache: "no-store" };

  if (mode === "revalidate") {
    const revalidate = Number.isFinite(n) && n > 0 ? n : 15;
    return { next: { revalidate } };
  }

  return { cache: "force-cache" };
}
export async function getProductById(id, cacheProfile) {
  const safeId = encodeURIComponent(String(id));
  const url = `${API_BASE}/products/${safeId}`;

  return fetchJson(url, toCacheOptions(cacheProfile));
}

Такой стенд полезен по двум причинам. Во-первых, он даёт наглядную витрину для самой темы кэширования. Во-вторых, не засоряет боевой интерфейс /goods и /goods/[id], где учебные маркеры в итоговой версии уже не нужны.

Как выбрать режим без долгой теории

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

Если данные меняются редко и источник нужно щадить, подходит force-cache. Это справочники, коллекции, публичные списки и всё, где небольшая задержка обновления допустима.

Если нужен рабочий компромисс для каталога, витрины или списка товаров, подходит next: { revalidate: N }. Именно этот вариант чаще всего оказывается базовым решением для публичных страниц. В guide по прежней модели кэширования route-level revalidate = false описан как поведение, близкое к бесконечному хранению для кэшируемых fetch, а положительное число задаёт частоту перевалидации маршрута, отдельные fetch могут уменьшать этот интервал. (Next.js)

Для Goods Finder практическим выбором стал режим revalidate: 60. Для каталога товаров это нормальный баланс между свежестью данных и числом запросов к внешнему API.

В итоге

Главная польза кэширования в Next.js начинается в момент, когда становится видно, что рендер страницы и свежесть данных это разные вещи. Один RenderStamp показывает, пересчиталась ли страница. Один data fetched показывает, пришёл ли новый ответ. После этого force-cache, no-store и revalidate перестают быть абстрактными флагами и превращаются в обычные инженерные решения с понятным поведением.

Статья опирается на проект Goods Finder. Пройти эти паттерны последовательно можно на курсе Next.js I: JavaScript 2026