Кэширование — не отдельная фича, а способность HTTP. Сервер может сказать клиенту: «этот ответ актуален N секунд, можешь не перезапрашивать». Браузер запомнит и при следующих запросах вместо сети возьмёт из своей памяти. Никакой код на фронте при этом не пишется — всё в заголовках.
Знать про это фронту нужно потому, что половина «почему у меня не обновляется» объясняется именно кэшем.
Главный игрок: Cache-Control
Заголовок в ответе сервера. Самые частые значения:
- Cache-Control: max-age=3600 — ответ можно кэшировать на 3600 секунд (час). Следующие запросы в течение этого времени браузер возьмёт из кэша, не дёргая сервер вообще.
- Cache-Control: no-cache — кэш использовать можно, но каждый раз нужно сначала уточнить у сервера, не изменился ли ответ. Звучит парадоксально (no — но кэшировать можно), а назван так не очень удачно. По смыслу это «всегда переспроси, перед тем как отдавать из кэша».
- Cache-Control: no-store — не кэшировать никогда и никак. Каждый запрос свежий. Используется для чувствительных данных вроде банковских балансов.
- Cache-Control: private — кэшировать можно, но только в личном кэше браузера, не в общих прокси и CDN (потому что в ответе персональные данные).
- Cache-Control: public — наоборот, можно кэшировать где угодно, в том числе в CDN.
Значения комбинируются: Cache-Control: public, max-age=86400 — «кэшируй на сутки, можно в CDN».
Условные запросы: ETag и If-None-Match
Часто хочется «кэшируй, но если ответ всё-таки изменился — присылай свежий». На это работает связка из двух заголовков.
Первый запрос:
GET /api/posts/42
→ HTTP/1.1 200 OK
Cache-Control: no-cache
ETag: "v3-9af2c1e8"
{"id": 42, "title": "Заметка", "body": "Текст..."}
Сервер прислал ресурс и в заголовке ETag — что-то вроде отпечатка пальца. Это идентификатор именно этой версии ответа. Браузер запомнил.
Через минуту делаем тот же запрос:
GET /api/posts/42
If-None-Match: "v3-9af2c1e8"
→ HTTP/1.1 304 Not Modified
ETag: "v3-9af2c1e8"
(тела нет)
Браузер сам подставил If-None-Match с тем ETag, что запомнил. Сервер сравнил со своей текущей версией: совпадает — значит, ничего не поменялось. Ответил кодом 304 без тела. Браузер взял данные из своего кэша.
Выгода: на ответ ушло несколько сотен байт вместо нескольких килобайт. Запрос всё равно состоялся, но трафик минимальный.
Если ответ изменился — сервер пришлёт обычный 200 с новым ETag и новым телом, как в первый раз.
Аналог: Last-Modified и If-Modified-Since
Старый механизм с теми же намерениями, но привязка не к отпечатку, а ко времени модификации:
// Первый ответ:
Last-Modified: Wed, 12 May 2026 14:00:00 GMT
// Следующий запрос:
If-Modified-Since: Wed, 12 May 2026 14:00:00 GMT
// Ответ, если ничего не менялось:
HTTP/1.1 304 Not Modified
На современных сервисах больше распространён ETag, потому что «отпечаток» точнее времени и не зависит от расхождения часов между серверами.
Кэш браузера vs кэш CDN
Когда речь о кэше, на пути запроса могут быть несколько слоёв:
- Кэш браузера — на конкретном устройстве, для конкретного пользователя.
- Кэш CDN/прокси — общий, между сервером и пользователями. Когда сайт «летает с раздачи Cloudflare», имеется в виду именно это.
- Кэш сервера приложения — внутренний (Redis, in-memory), к фронту прямого отношения не имеет.
Заголовок Cache-Control: private запрещает CDN кэшировать — нужно, когда в ответе персональные данные. Для общих ресурсов (картинки, скрипты, статичные данные) — ставим public, и трафик уходит на CDN.
Что фронту с этим делать
Обычно — ничего особенного. Заголовки кэширования настраивает бэкенд. Но иногда:
- Подозреваем кэш при отладке «почему не обновляется»: открываем DevTools → Network, ставим галочку Disable cache, перезагружаем. Если данные пошли свежие — виноват кэш.
- Принудительно объезжаем кэш: добавляем к URL параметр, который всегда разный: ?_=${Date.now()}. Браузер видит новый URL и пойдёт в сеть. Грязный трюк, но иногда нужен (например, для быстрого хотфикса).
- Указываем cache-опцию в fetch: fetch(url, { cache: 'no-store' }) — запрос пойдёт мимо кэша. Полезно для запросов, где всегда нужен свежий результат.
Краткая шпаргалка
| Заголовок | Где живёт | Что значит |
| Cache-Control: max-age=N | в ответе | «N секунд свежесть гарантирую — не дёргай меня» |
| Cache-Control: no-cache | в ответе | «перед использованием кэша всегда переспрашивай» |
| Cache-Control: no-store | в ответе | «не кэшируй вообще» |
| ETag: "X" | в ответе | «отпечаток моего текущего ответа — X» |
| If-None-Match: "X" | в запросе | «у меня версия X, ответь, только если другая» |
| 304 Not Modified | статус ответа | «не изменился — бери из кэша» |
Этого хватает на 95% задач. Глубокие нюансы кэширования (immutable, stale-while-revalidate, Vary) можно прочитать на MDN, когда станет интересно.