Глава 6

Кеширование

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

📖 ~20 мин Основы

Зачем нужно кеширование

Каждый HTTP-запрос — это сетевой обмен. Клиент отправляет запрос, ждёт пока он дойдёт до сервера, сервер обрабатывает, формирует ответ и отправляет обратно. Даже если всё работает быстро — это время, трафик и нагрузка на сервер.

Кеширование — это сохранение ответа, чтобы при повторном запросе не ходить за ним заново.

Без кеша — каждый раз
Клиент
↓ запрос
Сервер → обработка
↓ ответ
Клиент

↓ снова запрос
Сервер → обработка
↓ снова ответ
Клиент
С кешем — мгновенно
Клиент
↓ запрос
Кеш → сохраняет
↓ ответ
Клиент

↓ снова запрос
Кеш → ответ мгновенно
сервер не задействован

Выигрыш тройной:

Где живёт кеш

Кеш — не одно место, а несколько слоёв. Каждый работает на своём уровне. Запрос проходит сверху вниз. Если кеш на каком-то уровне отдаёт ответ — дальше запрос не идёт. Чем выше уровень отдал ответ — тем быстрее и дешевле.

🌐
Кеш браузера
Приватный
Самый близкий к пользователю. Браузер сохраняет ответы на диск. В DevTools → Network помечается как (disk cache) или (memory cache) — запрос до сервера не доходил. Хранит данные только для этого пользователя.
🌍
CDN
Общий
Географически распределённая сеть кеширующих серверов — расположены ближе к пользователям в разных городах. Пользователь в Москве получает ответ с московского узла, а не с сервера в Амстердаме. Кеширует в основном статику — картинки, CSS, JS, шрифты.
🔀
Кеш реверс-прокси
Общий
nginx, Varnish сохраняют ответы бэкенда и отдают следующим клиентам без обращения к бэкенду. Одна копия ответа для всех клиентов — именно поэтому нельзя кешировать персонализированные ответы (корзина, профиль). Один пользователь может получить данные другого.
🖥️
Бэкенд
Источник данных
Конечный источник. Запрос доходит сюда только если ни один кеш не отдал ответ.

Заголовки управления кешем

Кеш не работает сам по себе — сервер говорит клиентам и промежуточным узлам что можно кешировать и на сколько.

Cache-Control

Основной заголовок. Мы познакомились с ним в главе 2 — здесь разберём подробнее как он управляет поведением кеша на разных уровнях.

max-age=3600
Ответ считается свежим 3600 секунд с момента получения. В течение этого времени кеш отдаёт копию без обращения к серверу.
s-maxage=600
Отдельный max-age для общих кешей (CDN, прокси). Переопределяет max-age на промежуточных узлах. Браузер использует max-age, CDN — s-maxage.
public
Кеш разрешён на любом уровне: браузер, CDN, прокси. Для публичного контента — статика, каталоги.
private
Кеш разрешён только в браузере. Промежуточные узлы не сохраняют. Для персональных данных.
no-cache
Кешировать можно, но перед каждым использованием нужно спросить сервер — актуален ли ответ. Название обманчивое: no-cache не запрещает кеш, а запрещает использовать без проверки.
no-store
Не кешировать вообще. Ни браузер, ни CDN, ни прокси не сохраняют ответ. Для конфиденциальных данных — балансы, токены, персональные данные.

Практические примеры

Cache-Control
# Статика — кешировать на год (URL сменится при изменении файла)
Cache-Control: public, max-age=31536000

# API каталога — кешировать публично на час
Cache-Control: public, max-age=3600

# Данные текущего пользователя — не кешировать вообще
Cache-Control: private, no-store

# HTML-страница — проверять при каждом запросе
Cache-Control: no-cache

Expires и Pragma

Expires — предшественник Cache-Control: max-age. Указывает конкретную дату после которой ответ считается устаревшим. Если присутствуют оба — max-age имеет приоритет. Проблема: нужна синхронизация часов между клиентом и сервером — max-age надёжнее (использует относительное время).

Pragma: no-cache — рудимент из HTTP/1.0. В современном HTTP не нужен — Cache-Control полностью его заменяет. Встречается в старых системах.

Vary

Header
Vary: Accept-Encoding

Говорит кешу: «ответ зависит от конкретного заголовка запроса». Кеш должен хранить разные копии для разных значений этого заголовка.

ℹ️
Без Vary: Accept-Encoding кеш может отдать сжатый gzip-ответ клиенту, который не умеет его распаковать. Мы упоминали Vary в главе 2 в контексте Content-Encoding. В nginx директива gzip_vary on добавляет этот заголовок автоматически.

Условные запросы: проверка актуальности

max-age истёк — значит ли это что данные устарели? Не обязательно. Файл мог не измениться. Условные запросы позволяют проверить актуальность кеша без повторной загрузки данных.

Механизм: клиент говорит серверу «у меня есть версия от такого-то момента, она ещё актуальна?» Сервер отвечает: либо 304 Not Modified (тело не передаётся), либо 200 OK с полным новым ответом.

Два механизма условных запросов
Last-Modified / If-Modified-Since
1
Сервер отдаёт ответ с датой изменения:
Last-Modified: Wed, 18 Jun 2026 10:30:00 GMT
2
После истечения max-age клиент спрашивает:
If-Modified-Since: Wed, 18 Jun 2026 10:30:00 GMT
3
Сервер сравнивает даты:
304 Не изменился → без тела, клиент использует кеш
200 Изменился → полный новый ответ с телом
⚠ Точность до секунды — если файл менялся дважды за секунду, разницу не поймает.
ETag / If-None-Match
1
Сервер отдаёт хеш содержимого:
ETag: "a3f9c12e"
2
Клиент отправляет ETag обратно:
If-None-Match: "a3f9c12e"
3
Сервер сравнивает ETag:
304 ETag совпал → без тела
200 ETag другой → новый ответ + новый ETag
✓ Точнее Last-Modified — любое изменение содержимого даёт новый ETag. Подходит для API.
💡
Можно использовать оба одновременно. Если в запросе есть и If-None-Match, и If-Modified-Since — сервер по спецификации проверяет If-None-Match первым. nginx автоматически генерирует ETag и Last-Modified для статических файлов.

Жизненный цикл кеширования

Когда браузер делает запрос к ресурсу который когда-то кешировал — он проходит через два вопроса: свежесть и валидация.

Запрос к /style.css
? Есть в кеше?
├── НетGET /style.css → сервер → 200 OK → сохранить в кеш
└── Да
? max-age истёк?
├── Нет (свежий)отдать из кеша — запрос к серверу не нужен
└── Да (устаревший) → условный запрос:
If-None-Match / If-Modified-Since →
├── 304 Not Modified → отдать из кеша (обновить срок)
└── 200 OK → заменить в кеше, отдать новый

Два ключевых этапа:

Инвалидация кеша

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

⚠️
Почему это сложно Кеш браузера — на стороне клиента. Сервер не может залезть в браузер пользователя и удалить оттуда устаревшую копию. Если ответ был отдан с Cache-Control: max-age=86400 — браузер будет использовать кеш сутки, даже если данные на сервере изменились через минуту. Сервер бессилен.

«В программировании есть две по-настоящему сложные вещи — инвалидация кеша и именование переменных»

Стратегия 1 — Короткий max-age

Самый простой подход — не кешировать надолго. max-age=60 означает что кеш будет актуален максимум минуту, после чего браузер сделает условный запрос. Подходит для данных которые меняются часто, но повторные запросы в рамках минуты можно отдать из кеша.

Стратегия 2 — Версионирование URL (cache busting)

Самый надёжный подход для статических файлов. Вместо того чтобы менять содержимое файла по тому же URL — меняется сам URL. Хеш в имени файла — это хеш его содержимого. Изменился файл → изменился хеш → изменился URL → браузер запрашивает новый файл, старый кеш не мешает.

Cache busting — URL версионирование
/css/style.css max-age=60 старый подход — нельзя кешировать надолго

/css/style.a3f9c12e.css max-age=31536000 хеш в имени — кешируем на год
/js/app.b7d2e45f.js max-age=31536000 изменился файл — изменился хеш
index.html no-cache HTML всегда проверяем — содержит ссылки на актуальные хеши
💡
Инструменты сборки фронтенда (Webpack, Vite, Parcel) добавляют хеш в имена файлов автоматически. CSS и JS кешируются на год. HTML кешируется с no-cache чтобы браузер всегда получал свежую версию с актуальными ссылками на хешированные файлы.

Стратегия 3 — no-cache + ETag

Для API-ответов. Кеш хранит копию, но при каждом запросе делает условный запрос к серверу. Если данные не изменились — 304, трафик сэкономлен. Если изменились — 200 с новыми данными.

Стратегия 4 — Очистка кеша на прокси/CDN

Администратор или деплой-скрипт вызывает API CDN для сброса кеша определённых URL. Это не влияет на кеш браузера, но обновляет промежуточный слой немедленно.

Что нельзя кешировать

Не всё подлежит кешированию — некоторые ответы кешировать опасно или бессмысленно.

ЧтоПочему нельзяЗаголовок
Персональные данные Профиль, корзина, дашборд. Прокси-кеш сохранит ответ /api/me одного пользователя и отдаст другому — тот увидит чужие данные private, no-store
Ответы с Set-Cookie Один пользователь может получить куку другого. Большинство CDN и прокси по умолчанию не кешируют ответы с Set-Cookie private
POST, PUT, DELETE, PATCH Изменяющие операции. Кеширование их результатов бессмысленно — каждый вызов производит разный эффект не кешируются
Конфиденциальные данные Токены, балансы, медицинские данные — любые данные которые не должны попасть к другим лицам no-store

Кеширование в контексте проксирования

В главе 4 мы разобрали цепочку HAProxy → nginx → бэкенд. Где в этой цепочке место кешу и кто принимает решение что кешировать?

Кто решает что кешировать

Бэкенд. Именно бэкенд ставит Cache-Control, ETag, Last-Modified в ответ — потому что только он знает бизнес-логику: какие данные статичны, какие персонализированы, как часто меняются.

Реверс-прокси (nginx) и CDN подчиняются этим заголовкам. Они не принимают решения о кешировании самостоятельно — они читают заголовки и действуют по инструкции. Если бэкенд сказал no-store — прокси не кеширует. Если public, max-age=3600 — прокси может сохранить на час.

⚠️
Можно ли переопределить на прокси? Да — nginx позволяет игнорировать Cache-Control бэкенда и навязать свои правила через proxy_cache_valid и proxy_ignore_headers. Но это означает что прокси берёт на себя бизнес-решение которого не понимает — он не знает какие данные персонализированы, а какие нет. Опасная практика.

Итог

Кеширование работает на нескольких уровнях: браузер (приватный), реверс-прокси и CDN (общий). Бэкенд принимает решение о кешировании — через заголовки ответа.

Два этапа

ЭтапМеханизмЧто делает
Свежесть Cache-Control: max-age Можно ли использовать кеш без обращения к серверу
Валидация ETag / Last-Modified Проверяет актуальность устаревшего кеша условным запросом (304 или 200)

Основные заголовки

ЗаголовокРоль
Cache-ControlГлавный — указывает правила кеширования
ETagИдентификатор версии содержимого
Last-ModifiedДата последнего изменения
If-None-MatchУсловный запрос по ETag
If-Modified-SinceУсловный запрос по дате
VaryОт какого заголовка запроса зависит ответ

Главная сложность — инвалидация: сервер не может удалить устаревший кеш из браузера. Решение для статики — версионирование URL (хеш в имени файла). Для API — короткий max-age или no-cache с условными запросами.