HTTP-кэширование: как браузер решает, использовать ли сохранённый ответ

HTTP-кэширование: как браузер решает, использовать ли сохранённый ответ

HTTP-кэширование часто описывают слишком просто: браузер «сохранил файл» и потом взял его локально. На практике решение состоит из нескольких отдельных проверок. Сначала нужно понять, разрешено ли сохранять ответ, затем — остаётся ли сохранённая копия свежей, а если срок свежести закончился — можно ли быстро проверить её у исходного сервера без повторной передачи всего тела ответа.

Поэтому Cache-Control, ETag и Last-Modified решают разные задачи. Cache-Control задаёт правила хранения и повторного использования ответа, а ETag и Last-Modified помогают проверить, изменилась ли уже сохранённая версия. Если отдельно нужно разобраться, как сервер сообщает клиенту тип данных в теле ответа, это связано с другим заголовком — Content-Type и MIME-типами.

Полезнее думать не о «кэше», а о четырёх решениях подряд

При повторном обращении к одному URL клиенту или промежуточному кэшу нужно ответить примерно на четыре вопроса.

  1. Можно ли хранить этот ответ? На это влияют метод запроса, статус ответа и директивы кэширования.
  2. Если ответ уже сохранён, он ещё свежий? Пока срок свежести не закончился, подходящую копию можно использовать без обращения к исходному серверу.
  3. Если копия устарела, можно ли её проверить? Для этого используются валидаторы, прежде всего ETag и Last-Modified.
  4. Что ответил сервер на проверку? Если представление не изменилось, сервер может вернуть 304 Not Modified без нового тела. Если изменилось — приходит новый обычный ответ, например 200 OK с актуальным содержимым.

Эта последовательность важнее запоминания отдельных директив. Она сразу показывает, почему «хранить», «использовать без проверки» и «проверить актуальность» — не одно и то же действие.

Cache-Control управляет повторным использованием ответа

Поле Cache-Control содержит директивы для кэшей. Одна из самых понятных — max-age: она задаёт, сколько секунд после генерации ответа тот может считаться свежим в соответствующем кэше.

Cache-Control: max-age=3600

Такой заголовок не означает, что ресурс обязательно будет храниться ровно час и гарантированно останется в памяти браузера. Он сообщает, что подходящую сохранённую копию можно считать свежей в пределах заданного времени. Кэш может удалить запись раньше, например из-за собственных ограничений хранения.

У нескольких часто используемых директив принципиально разные значения:

Директива Что означает на практике Что важно не перепутать
max-age=N Задаёт срок свежести ответа в секундах Это не гарантия фактического хранения записи всё это время
no-cache Разрешает хранить ответ, но перед повторным использованием требует успешной проверки, когда директива применима Название не означает «никогда не сохранять»
no-store Запрещает кэшу сохранять ответ Это более жёсткое правило, чем no-cache
private Разрешает хранение приватным кэшем, например браузером конкретного пользователя, но запрещает хранение общим кэшем Особенно важно для персонализированных ответов
public Явно разрешает хранение ответа общими кэшами при соблюдении остальных условий Не делает любой ответ автоматически безопасным для общего кэша
must-revalidate После устаревания требует проверить сохранённый ответ перед обычным повторным использованием Не задаёт срок свежести само по себе

Свежий ответ и проверенный ответ — разные состояния

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

Когда срок свежести закончился, сохранённая копия не обязана сразу выбрасываться. Она может стать кандидатом на повторную проверку. Здесь как раз появляются валидаторы.

Главная практическая разница выглядит так:

свежая копия
→ использовать сразу

устаревшая копия + валидатор
→ условный запрос к серверу
→ 304 Not Modified или новый 200 OK

устаревшая копия без пригодной проверки
→ получить актуальный ответ заново

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

ETag проверяет версию представления

ETag — это непрозрачный идентификатор текущего представления ресурса. Формат значения определяет сервер; клиенту не нужно знать, является ли внутри хэш, номер версии или другой маркер. Важно только, чтобы сервер последовательно сравнивал значения по правилам HTTP.

HTTP/1.1 200 OK
Cache-Control: max-age=600
ETag: "v42"

Когда сохранённый ответ потребует проверки, клиент может отправить известное значение через If-None-Match:

GET /app.css HTTP/1.1
If-None-Match: "v42"

Если выбранное представление по-прежнему соответствует этому тегу, сервер может ответить:

HTTP/1.1 304 Not Modified
ETag: "v42"

Тело ресурса заново не передаётся: клиент продолжает использовать уже сохранённую копию. Если версия изменилась, сервер возвращает актуальное представление обычным ответом.

У ETag есть сильная и слабая форма. Слабый тег помечается префиксом W/ и означает, что представления могут считаться эквивалентными для слабого сравнения, даже если они не идентичны побайтно. Это различие важно не во всех сценариях кэширования, но его нельзя игнорировать там, где требуется точное сравнение представлений или работа с диапазонами байтов.

Last-Modified использует время последнего изменения

Last-Modified сообщает дату и время, когда исходный сервер считает выбранное представление изменённым в последний раз.

Last-Modified: Sun, 06 Sep 2026 10:30:00 GMT

При повторной проверке клиент может передать это значение через If-Modified-Since:

GET /manual.pdf HTTP/1.1
If-Modified-Since: Sun, 06 Sep 2026 10:30:00 GMT

Если более новой версии нет, возможен ответ 304 Not Modified. Если представление изменилось, сервер отправит новую версию.

Last-Modified удобен тем, что может естественно опираться на дату изменения файла или другого источника данных. Но временная метка не всегда различает быстрые изменения достаточно точно. Поэтому HTTP отдельно предусматривает entity tags, а сервер при возможности может отправлять оба валидатора.

Если есть и ETag, и Last-Modified

В одном ответе могут присутствовать оба поля:

HTTP/1.1 200 OK
Cache-Control: no-cache
ETag: "page-184"
Last-Modified: Sun, 06 Sep 2026 10:30:00 GMT

При последующей проверке клиент может отправить и If-None-Match, и If-Modified-Since. Для проверки кэшированного ответа условие с If-None-Match имеет приоритет перед If-Modified-Since. Это позволяет использовать более точный валидатор, не отказываясь от даты последнего изменения там, где она также полезна.

Проверка кэша по ETag и дате

Почему no-cache и no-store нельзя использовать как синонимы

Это одна из самых частых ошибок в конфигурации.

Cache-Control: no-cache не запрещает сохранить ответ. Смысл в другом: сохранённую копию нельзя просто считать пригодной к использованию без предусмотренной проверки. В сочетании с ETag или Last-Modified это даёт удобный режим для документов, которые должны оставаться актуальными, но при этом не требуют повторной передачи полного тела, если ничего не изменилось.

Сравнение no-cache и no-store

Cache-Control: no-store, напротив, запрещает хранить ответ в кэше. Такой режим нужен не «для надёжности по умолчанию», а когда хранение конкретного ответа действительно нежелательно.

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

HTML и версионированные ресурсы часто требуют разных политик

Одна универсальная настройка для всех URL обычно неудобна. Например, основной HTML-документ может менять содержимое, сохраняя тот же адрес. Для такого ресурса часто разумна политика, при которой клиент проверяет актуальность и при неизменившейся версии получает 304.

Cache-Control: no-cache
ETag: "page-184"
Last-Modified: Sun, 06 Sep 2026 10:30:00 GMT

У статических CSS, JavaScript или изображений ситуация может быть другой. Если в URL встроена версия или отпечаток содержимого и при каждом изменении файла создаётся новый URL, старую версию можно кэшировать намного дольше: изменение самого адреса становится механизмом получения новой версии.

/assets/app.8f31c2.js
Cache-Control: public, max-age=31536000, immutable

Ключевое условие здесь не число секунд само по себе, а договорённость: содержимое такого URL после публикации не меняют. Если под тем же адресом незаметно заменить файл, длинный срок свежести может оставить часть клиентов со старой копией до окончания её срока или удаления записи из кэша.

Персонализированные ответы требуют отдельной проверки

Ответ, который зависит от авторизации, cookies или данных конкретного пользователя, нельзя механически обрабатывать как общий статический ресурс. Для таких страниц сначала нужно определить, допускается ли хранение вообще и каким кэшем. Директива private позволяет ограничить хранение приватным кэшем пользователя, но выбор политики зависит от содержания и требований конкретного приложения.

Особенно опасно автоматически считать, что public или большой max-age безопасны для любого ответа только потому, что они ускоряют повторную загрузку. Перед настройкой общего кэша нужно проверить, не меняется ли представление в зависимости от пользователя, заголовков запроса или другого контекста.

Как диагностировать кэширование по фактическому обмену

Начинать лучше не с конфигурационного файла сервера, а с ответа, который реально получает клиент.

  1. Откройте нужный URL в панели Network браузера и посмотрите Cache-Control, ETag, Last-Modified, статус и фактический источник ответа.
  2. Проверьте первый ответ: можно ли понять срок свежести и есть ли валидатор.
  3. Повторите запрос после того, как копия должна потребовать проверки.
  4. Посмотрите, появился ли If-None-Match или If-Modified-Since.
  5. Проверьте результат: 304 Not Modified при неизменившейся версии или новый 200 OK с актуальным телом.
  6. Отдельно проверьте, что персонализированные ответы не попадают в общий кэш.

Для быстрой проверки заголовков пригодится и командная строка:

curl -I https://example.com/page

curl -I \
  -H 'If-None-Match: "v42"' \
  https://example.com/page

Конкретный результат зависит от конфигурации сервера, промежуточных прокси и текущего состояния ресурса. Поэтому наличие 304 не стоит проверять по одному заранее придуманному сценарию: сначала нужно посмотреть реальные заголовки первого ответа и только потом строить условный запрос.

Ошибки, которые проще увидеть по этой схеме

  • no-cache трактуют как запрет хранения. В результате диагностика строится на неверной модели.
  • Длинный max-age ставят на URL, содержимое которого меняют без смены адреса. Клиент не обязан узнавать о новой версии, пока старая копия остаётся свежей.
  • ETag меняется без содержательного изменения ресурса. Тогда повторная проверка теряет часть смысла: сервер постоянно считает сохранённую версию другой.
  • Last-Modified формируется нестабильно. Если дата не отражает реальное изменение представления, условные запросы дают ненадёжный результат.
  • Смешивают браузерный кэш, общий прокси-кэш и прикладные механизмы хранения. У них разные области действия, поэтому один и тот же ответ нужно оценивать в конкретном контексте.
  • Проверяют только конфигурацию сервера. Между приложением и пользователем могут быть reverse proxy, CDN или другие промежуточные компоненты; важен итоговый HTTP-ответ.

Короткая схема выбора

Ситуация Что проверить в первую очередь
Ресурс может долго жить под неизменяемым версионированным URL Длительный срок свежести и смена URL при каждом изменении содержимого
URL остаётся прежним, но содержимое может обновляться Политика свежести плюс ETag и/или Last-Modified для повторной проверки
Ответ персонализирован Разрешено ли хранение и должен ли доступ иметь только приватный кэш
Ответ вообще нельзя сохранять Нужна ли директива no-store
Есть жалоба «браузер показывает старую версию» Фактические Cache-Control, возраст копии, валидаторы, URL ресурса и результат условного запроса

Итог

HTTP-кэширование проще диагностировать, если разделять три вопроса: можно ли сохранить ответ, можно ли использовать его без сети и можно ли проверить сохранённую версию без повторной передачи всего ресурса. Cache-Control в основном управляет правилами хранения и свежести, а ETag и Last-Modified дают серверу и клиенту механизм повторной проверки. Ответ 304 Not Modified — не «особый кэш сам по себе», а результат успешного условного запроса, после которого клиент продолжает использовать уже имеющееся представление.

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

Источники