Почему fetch возвращает старые данные — 3 причины кэша и как их починить
Ты поменял цену в базе. Открываешь сайт — старая цена. Обновляешь страницу — старая. Перезаходишь — старая. А локально всё работало: поменял, обновил, увидел.
Знакомо? Где-то по дороге между базой и экраном сидит кэш и отдаёт тебе вчерашний ответ. Хитрость в том, что кэшей на этой дороге несколько, и каждый прячется по-своему.
Вот три места, где он сидит чаще всего. Начнём с самого частого.
Причина 1. Next.js выполнил fetch один раз — при сборке
Это лидер жалоб, особенно в проектах, собранных с ИИ на Next.js. Симптом очень узнаваемый: в режиме разработки данные свежие, а после деплоя — застыли. Застыли ровно на моменте деплоя.
Что происходит. Next.js старается сделать каждую страницу статической. Если на странице нет ничего, что зависит от конкретного запроса (cookies, заголовков, параметров поиска), он выполняет её один раз во время next build. Твой fetch тоже выполняется один раз. Результат запекается в готовый HTML. Дальше все посетители получают этот HTML, а база больше не спрашивается.
В режиме разработки Next.js этого не делает и рендерит страницу на каждый запрос. Поэтому локально ты ничего не замечаешь. Классический случай из разряда «локально работает, а на сервере нет».
Как проверить. Запусти next build и посмотри на таблицу маршрутов в конце. Значок ○ рядом со страницей значит «Static», собрана заранее. Значок ƒ значит «Dynamic», собирается на каждый запрос. Твоя страница с ценами под кружком — вот и ответ.
Как починить. Скажи Next.js, что этим данным нужна свежесть. Есть три уровня:
// 1) один запрос — всегда свежий
const res = await fetch(url, { cache: "no-store" })
// 2) вся страница — обновлять не чаще раза в минуту
export const revalidate = 60
// 3) вся страница — рендерить на каждый запрос
export const dynamic = "force-dynamic"
Второй вариант часто лучший. Цены и каталог не обязаны быть свежими до секунды, а сайт остаётся быстрым.
И ещё. В Next.js 14 и более ранних версиях fetch на сервере по умолчанию кэшировался навсегда. С версии 15 — нет. ИИ, обученный на старых примерах, может написать код в расчёте на одно поведение, а у тебя будет другое. Проверь версию в package.json.
Причина 2. Браузер закэшировал ответ сам
Тут ловушка, о которой мало кто знает. Даже если сервер ничего не сказал про кэширование, браузер всё равно может закэшировать ответ.
Стандарт HTTP разрешает «эвристику». Если в ответе нет заголовка Cache-Control, но есть Last-Modified, браузер прикидывает срок свежести сам. Типичная оценка — 10% от возраста данных. Файл не менялся десять дней? Браузер может спокойно считать его свежим целые сутки и не ходить на сервер.
Как проверить. Открой DevTools (F12) → вкладка Network → повтори действие. В колонке Size у запроса будет написано (disk cache) или (memory cache). Значит, до сервера он вообще не дошёл. Кликни на запрос и посмотри заголовки ответа: Cache-Control, Expires, Last-Modified.
Как починить. Правильное место — сервер. Для API с живыми данными отдавай заголовок:
Cache-Control: no-store
Осторожно с похожим no-cache. Он не значит «не кэшировать». Он значит «кэшируй, но перед каждым использованием спроси сервер, не устарело ли». Для живых данных это тоже рабочий вариант, просто медленнее на один запрос.
Если сервер не твой, можно попросить свежесть со стороны клиента: fetch(url, { cache: "no-store" }). Браузер пойдёт прямо в сеть.
Причина 3. Ответ запомнил CDN или service worker
Между тобой и сервером может стоять CDN — Cloudflare, Vercel, Netlify. Его работа — помнить ответы и раздавать их быстро. Иногда он помнит и твой API.
А если сайт — PWA, на пути есть ещё service worker. Это скрипт в браузере, который перехватывает запросы и может отвечать из своего хранилища. Даже без интернета.
Как проверить. В заголовках ответа ищи cf-cache-status: HIT (Cloudflare) или x-vercel-cache: HIT (Vercel). HIT значит «отдано из кэша CDN». Заголовок age: 3600 скажет, сколько секунд ответ уже лежит в кэше. Service worker выдаст себя в колонке Size: там будет (ServiceWorker).
Как починить. Для CDN — тот же Cache-Control: no-store или private на ответах API. Если нужно срочно — сбрось кэш в панели CDN. Для service worker — настрой запросы к API по схеме network-first: сначала сеть, кэш только когда сети нет.
Можно просто добавить ?t=Date.now() к адресу?
Сработает: для любого кэша это будет новый адрес. Но это пластырь. Ты прячешь причину, а заодно отключаешь полезный кэш там, где он был нужен. Лучше один раз поставить правильный заголовок на сервере.
А если данные старые только после действия пользователя?
Тогда, скорее всего, дело не в HTTP-кэше, а в кэше на клиенте. Библиотеки вроде React Query и SWR держат ответы в памяти и считают их свежими какое-то время. После сохранения формы скажи им обновить данные: в React Query — invalidateQueries, в SWR — mutate.
Почему после жёсткого обновления всё стало нормально?
Ctrl+Shift+R заставляет браузер пропустить свой кэш. Если это помогло, скорее всего, у тебя причина №2. У посетителей так не будет: они не жмут жёсткое обновление. Чини заголовки на сервере.
Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.




