awesome-everything EN
↑ Обратно к восхождению

Кеширование

Cache-Control: заголовок, который программирует каждый кэш в цепочке

Суть no-cache не отключает кэширование, private всё равно может утечь в CDN, а пропущенный s-maxage отдаёт CDN неверный TTL. Директивы говорят браузеру, прокси и CDN, как именно вести себя — и дефолты тут ловушки.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 16 min

Дашборд банка выкатывает Cache-Control: no-cache на эндпоинте баланса счёта. Ревью безопасности кивнуло — «no-cache, ничего не хранится». Через неделю общий киоск показывает одному клиенту баланс предыдущего после нажатия «Назад». Браузер всё это время кэшировал ответ: no-cache никогда не значило «не кэшируй». Оно значило «кэшируй, но спрашивай перед повторным использованием». На bfcache и навигации назад/вперёд этот «спрос» так и не сработал. Директива, которая реально предотвратила бы хранение, — это no-store, и её никто не выкатил.

Два ответа на два разных вопроса

Cache-Control — это не один тумблер, а набор директив, и первое дело — знать, какая директива отвечает на какой вопрос. Два вопроса, которые разработчики постоянно путают: «можно ли кэшу это хранить?» и «можно ли кэшу переиспользовать хранимую копию без проверки?»

no-store отвечает на первый: не записывай это ни на диск, ни в память нигде. Используй для по-настоящему чувствительных, per-request данных — балансов, страниц сброса пароля, всего, что не должно осесть в bfcache киоска.

no-cache отвечает на второй: хранить можно, но перед выдачей обязательно ревалидируй с origin (условный запрос If-None-Match/If-Modified-Since). Если origin вернул 304 Not Modified, кэш отдаёт хранимое тело бесплатно; если контент изменился — отдаёт новый. Это не «выключено» — это «всегда сначала проверь». Имя, которое люди ожидали от no-cache, по смыслу ближе к тому, что делает no-store, — именно поэтому путаница повсеместна. Значительная доля ответов с no-cache в реальном трафике уходит без ETag или Last-Modified, то есть даже эффективно ревалидировать не может — сильный сигнал, что автор имел в виду no-store.

max-age, s-maxage и CDN, который тебя игнорирует

max-age=<секунды> задаёт время свежести: как долго любой кэш может отдавать копию без ревалидации. s-maxage делает то же самое, но только для общих кэшей — CDN и обратных прокси, — и для них он перекрывает max-age. Приватные (браузерные) кэши игнорируют s-maxage полностью.

Именно этот раздел отличает сеньора. Ловушка: ты ставишь max-age=0, чтобы держать HTML всегда свежим в браузерах, но забываешь s-maxage. CDN, не имея s-maxage, откатывается к max-age=0 и ревалидирует на каждый запрос — твой origin под нагрузкой, а CDN ничего не делает. Или наоборот: max-age=31536000 на HTML без s-maxage, и CDN радостно кэширует персонализированную страницу залогиненного пользователя на год, отдавая её всем. Эти два числа — независимые рычаги, потому что у двух уровней кэша разные задачи: браузер кэширует для одного пользователя, CDN — для всех.

ДирективаЧто она делает на самом делеЛовушка
no-storeНи один кэш не хранит ответ вообщеТо, что ты хотел, когда тянулся к no-cache
no-cacheХранит, но ревалидирует перед каждым переиспользованиемНЕ запрещает хранение; бесполезно без ETag
max-age=NСвежо N секунд в любом кэшеCDN тоже его берёт, если не перекрыть s-maxage
s-maxage=NСвежо N секунд только в общих кэшахБраузеры игнорируют; забыть = отдать CDN max-age
privateХранит только браузер; никаких общих кэшейЗабыть = CDN кэширует страницу одного для всех
immutableПропускает ревалидацию полностью, пока свежоБезопасно только с content-hash, неизменными URL

private vs public и утечка, которая ломает карьеры

public говорит: любой кэш может это хранить, даже при наличии заголовка Authorization. private говорит: хранить может только браузер одного пользователя — CDN или общий прокси не должны. Провал жесток и тих: аутентифицированный, персонализированный ответ (/account, залогиненная главная, API, отдающее текущего пользователя) уходит без private, CDN кэширует копию первого пользователя, и каждому следующему посетителю отдают чужие данные. Это хрестоматийный инцидент cache-poisoning / утечки данных, и он почти всегда сводится к дефолтному Cache-Control, оставшемуся на роуте, который позже стал персонализированным.

Рефлекс сеньора: любой ответ, который зависит от того, кто спрашивает, — это private, no-store или private, max-age=0, и ты предполагаешь, что CDN стоит перед тобой, видишь ты его или нет. Затем Vary: Cookie или Vary: Authorization как страховочный сигнал, чтобы общий кэш ключевался по учётке.

Почему это работает

Vary — это ключ кэша, а не директива времени жизни. Vary: Accept-Encoding нормально — два варианта (gzip, br). Но Vary: User-Agent или Vary: Cookie с высокой кардинальностью значений фрагментируют кэш так сильно, что hit rate падает почти до нуля: каждое отдельное значение получает свою запись, и ничего не переиспользуется. Хуже: некоторые CDN (Cloudflare) игнорируют Vary на большинстве ответов, поэтому Vary, на который ты опираешься ради корректности, может вообще не учитываться — а значит реальную безопасность должны нести private/no-store, а не Vary.

immutable, content-hash и единственный способ кэшировать на год

Для статических ассетов цель противоположна HTML: кэшировать настолько жёстко, насколько физически возможно. Стандарт — Cache-Control: public, max-age=31536000, immutable — год, привычное значение для неизменяемых ассетов (RFC не задаёт верхнего предела), плюс immutable, который велит браузеру пропустить даже условную ревалидацию, которую он иначе сделал бы на жёстком перезагрузе. Это экономит round trip на ассет, что важно на странице с 80 файлами.

Но годовой кэш — это годовой баг, если файл когда-нибудь придётся менять. Паттерн, делающий это безопасным, — имена файлов с content-hash: app.4f3a9c.js вместо app.js. Хэш выводится из байтов, поэтому изменённый файл получает новое имя и, значит, новый URL — старый URL остаётся immutable и закэшированным навсегда, новый URL запрашивается свежим. Поэтому бандлеры (Vite, webpack, esbuild) по умолчанию выдают хэшированные имена: именно это позволяет immutable + годовому max-age быть и агрессивным, и корректным. Правило, которое всё связывает: хэшируй ассеты, никогда HTML. HTML — это no-cache (или max-age=0, must-revalidate), чтобы он всегда подхватывал новые URL ассетов; ассеты immutable, чтобы их никогда не перезапрашивали.

must-revalidate — более строгий родственник: став несвежим, кэш обязан ревалидировать и не может отдать несвежую копию, даже если origin недоступен. Противоположная позиция — stale-while-revalidate и stale-if-error — это явное согласие отдавать несвежий контент. stale-while-revalidate=600 значит «отдавай несвежую копию мгновенно до 600с после истечения, пока ревалидируешь в фоне», меняя слегка устаревший ответ на нулевую задержку для запроса пользователя. stale-if-error=86400 значит «если origin лежит, отдавай несвежее сутки, а не показывай ошибку». Реалистичный составной заголовок для API за CDN: max-age=60, s-maxage=300, stale-while-revalidate=3600 — 60с свежо в браузерах, 5 минут в CDN, затем до часа мгновенно-но-несвежо, пока в фоне идёт обновление.

Выбери лучший вариант

Ты отдаёшь персонализированный HTML-дашборд залогиненного пользователя через CDN. Выбери Cache-Control.

Викторина

Разработчик хочет, чтобы ответ API нигде не хранился. Он выкатывает Cache-Control: no-cache. Что произойдёт на самом деле?

Викторина

Ты ставишь Cache-Control: max-age=0 на HTML, чтобы держать его свежим, но впереди стоит CDN. В чём пробел?

Расставь шаги по порядку

Расставь вопросы для выбора правильного Cache-Control для ответа:

  1. 1 Чувствительное / per-request? Если да → no-store и остановись.
  2. 2 Зависит от пользователя (auth/cookie)? Если да → private (держи вне общих кэшей).
  3. 3 Это content-hash статический ассет? Если да → public, max-age=31536000, immutable.
  4. 4 Иначе выбери свежесть: max-age для браузеров, s-maxage для CDN (часто дольше).
  5. 5 Нужна устойчивость? Добавь stale-while-revalidate / stale-if-error, чтобы отдавать несвежее вместо медленного/ошибки.
Вспомните перед уходом
  1. 01
    Коллега ставит no-cache на чувствительный эндпоинт, чтобы тот не хранился. Объясни, почему это неверно и что использовать.
  2. 02
    Разбери, почему content-hash имена файлов позволяют безопасно кэшировать ассеты на год с immutable и что НЕЛЬЗЯ кэшировать так же.
Итог

Cache-Control — это набор директив, отвечающих на два отдельных вопроса: можно ли кэшу это хранить и можно ли переиспользовать хранимую копию без проверки, — и их смешение источник большинства боевых инцидентов кэша. no-store — единственная директива, предотвращающая хранение; no-cache разрешает хранение и лишь форсирует ревалидацию, поэтому выкат no-cache на чувствительных данных утекает их в браузер и bfcache. max-age задаёт свежесть для любого кэша; s-maxage перекрывает его только для общих кэшей, поэтому забыть s-maxage значит отдать CDN браузерный TTL и либо завалить origin, либо закэшировать персональные страницы для всех. private держит per-user ответы вне общих кэшей, и его отсутствие на аутентифицированном роуте — классическая утечка данных через CDN. Vary задаёт ключ кэша, а не время жизни, и Vary с высокой кардинальностью обрушивает hit rate, тогда как некоторые CDN игнорируют Vary вовсе, — поэтому безопасность должна жить в private/no-store, а не в Vary. Наконец, content-hash имена файлов делают public, max-age=31536000, immutable и агрессивным, и корректным: хэшируй ассеты, чтобы изменение означало новый URL, и никогда не кэшируй HTML, который на них указывает.

Продолжить восхождение ↑Cache-Control: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.