В этой статье я изложу то, что почерпнул из чтения RFC 9111 (2022 год) — последнего стандарта по HTTP-кэшингу.
Он определяет HTTP-заголовок Cache-Control, предписывающий способ хранения и повторного использования HTTP-запросов касательно не только кэша браузера, но и всех промежуточных кэшей наподобие прокси и CDN, которые могут существовать между клиентом и исходным сервером.

В заголовке Cache-Control может содержаться множество разделённых запятыми директив, часть из которых должна добавляться к HTTP-запросам, а другая — к HTTP-ответам. Вот как выглядит типичный заголовок ответа:
HTTP/2 200
Cache-Control: max-age=0, must-revalidateНекоторые из этих директив предназначены конкретно для общих кэшей, то есть промежуточных кэшей, отправляющих одинаковые кэшированные запросы многим пользователям, в то время как другие относятся к приватным кэшам, например, к кэшу браузера.
Что считается свежим?
Когда кэш получает запрос, он должен понять, по-прежнему ли кэшированный ответ остаётся свежим (в этом случае его можно использовать повторно, экономя на генерации HTTP-запроса), или он уже устарел и его нужно валидировать на сервере.
Для определения актуальности кэш сравнивает время жизни ответа с так называемым сроком свежести (freshness timeline) ответа.
Время жизни кэшированного запроса — это время, прошедшее после его последней генерации или повторной ревалидации исходным сервером. Ко времени хранения в своём собственном кэше браузер прибавляет все заголовки Age: <seconds>, полученные от промежуточных кэшей.
Срок свежести — это промежуток времени, после которого кэшированный запрос считается устаревшим. Обычно он указывается сервером при помощи соответствующих заголовков ответов, но в случае отсутствия явного валидного указания кэш может его вычислять приблизительно в следующем порядке приоритета:
сервер указывает срок свежести в секундах при помощи директивы
Cache-Control: max-age=<number>в ответе; в противном случае,кэш вычисляет интервал между заголовками ответов
Expires: <date>иDate: <date>, если они существуют; в противном случае,если заголовок
Expiresотсутствует, у ответа нет явно указанного срока свежести, поэтому можно применить эвристику свежести на основании заголовка ответаLast-Modified.
Для общих кэшей наивысшим приоритетом обладает специальная директива s-maxage=<number>.
Что происходит после завершения срока
Даже если ответ устарел, его необязательно удалять.
Когда кэш получает запрос устаревшего кэшированного ответа, он должен валидировать его со своим апстрим-сервером. Хотя при валидации всегда генерируется HTTP-запрос, при этом не происходит передачи данных, если на сервере нет более новой версии кэшированного ответа, поэтому это всё равно может быть быстрее, чем обычный запрос.
При валидации применяется механизм так называемого условного HTTP-запроса (conditional HTTP request), содержащего один или несколько специальных заголовков, называемых предусловиями (precondition):
если выполнено предусловие с наивысшим приоритетом, то сервер отвечает HTTP 200 OK и обновлённым телом ответа; в противном случае,
он отвечает HTTP 304 Not Modified и пустым телом, подтверждая, что можно повторно использовать уже имеющийся ответ.
Для генерации предусловий, необходимых для этих условных запросов, ответы необходимо помечать способом, уникальным для каждой версии:
в прошлом это делалось с помощью заголовка
Last-Modified: <date>, соответствующего последнему обновлению контента;более гибкой и надёжной альтернативой будет заголовок
ETag: "<value>", хранящий произвольную ASCII-строку, уникальным образом идентифицирующую запрос. Эта строка обычно представляет собой хэш, включающий в себя один или несколько аспектов: время изменения, размер файла и содержимое файла.
При выполнении валидации заголовки кэшированных запросов используются в качестве предусловий условного запроса:
Last-Modified: <date>превращается вIf-Modified-Since: <date>;ETag: "<value>"превращается вIf-None-Match: "<value>".
При наличии обоих предусловий используется If-None-Match.
Вне зависимости от результата запроса валидации, заголовки кэшированных запросов переписываются новыми значениями, полученными от сервера, а счётчик свежести кэшированного запроса сбрасывается.
В некоторых обстоятельствах возможно создание кэшей для отправки устаревших ответов, например, при потере связи с сервером или в случае серверной ошибки HTTP 5XX. Также существуют директивы Cache-Control, влияющие на способ обработки устаревших запросов; подробнее о них мы поговорим в следующем разделе.
Директивы ответов Cache-Control
max-age=
Директива ответов max-age определяет срок свежести ответа в секундах, после которого ответ должен считаться устаревшим. ☞ RFC 9111 § 5.2.2.1
must-revalidate
Директива ответо�� must-revalidate указывает, что кэш не должен повторно использовать устаревший ответ, пока он не будет успешно валидирован исходным сервером. ☞ RFC 9111 § 5.2.2.2
Если сервер выбрасывает ошибку, кэш должен выдать её вместо устаревшего ответа. Если кэш отключён, он должен сгенерировать ошибку с кодом статуса HTTP 504 Gateway Timeout, или другим, более подходящим кодом ошибки.
Побочные эффекты: must-revalidate — это одна из тех директив наряду с s-maxage и public, которые позволяют общим кэшам хранить и повторно использовать ответ на запрос, содержащий заголовок Authorization, что в обычном случае им делать запрещено.
no-cache
Директива ответов no-cache указывает, что кэш не должен повторно использовать никакие ответы, пока их успешно не валидирует исходный сервер. ☞ RFC 9111 § 5.2.2.4
Она схожа с must-revalidate, но касается всех кэшированных ответов, а не только устаревших. По сути, no-cache — это что-то вроде max-age=0, must-revalidate.
no-store
Директива ответов no-store указывает, что приватные и общие кэши не должны хранить никакие части запроса или ответа, а также никогда не должны повторно использовать ответ. Однако стандарт сразу предупреждает, что его эффект не гарантирован:
«НЕ ДОЛЖЕН хранить» в этом контексте означает, что кэш НЕ ДОЛЖЕН намеренно сохранять информацию в постоянном хранилище и ДОЛЖЕН предпринимать все усилия для как можно скорейшего удаления информации из временного хранилища после её передачи. Эта директива не является надёжным и достаточным механизмом для обеспечения приватности. В частности, вредоносные или скомпрометированные кэши могут не признавать эту директиву и не подчиняться ей, а коммуникационные сети могут быть уязвимы к прослушке. ☞ RFC 9111 § 5.2.2.4
Побочные эффекты: эта директива также может влиять и на кэши, не связанные с HTTP. Большинство браузеров исключает из кэша перемещения вперёд/назад страницы, содержащие директиву ответов no-store. Однако Chrome недавно начал делать часть таких кэшей доступной для bfcache, если браузер сочтёт это безопасным.
must-understand
Директива ответов must-understand указывает, что кэш не должен хранить и повторно использовать овтеты с кодами статусов HTTP, семантику которых кэш не понимает и не подчиняется им. Эта директива предназначена для защиты на будущее существующих реализаций от кодов статусов, которые могут иметь особые требования к кэшированию. ☞ RFC 9111 § 5.2.2.3
Рекомендуется использовать must-understand, no-store вместе в качестве меры отката; стандарт также рекомендует кэшам игнорировать директиву no-store, если они не понимают семантику кода статуса HTTP. Благодаря этому старые кэши, не понимающие директиву must-understand, совершенно не будут кэшировать ответ, хотя на 2025 год таких кэшей, вероятно, осталось крайне мало.
private
Директива ответов private указывает, что ответ предназначается для одного пользователя. ☞ RFC 9111 § 5.2.2.7.
Следовательно:
общий кэш не должен хранить ответ; и
приватный кэш может хранить ответ, даже если ответ эвристически не должен был бы кэшироваться.
Директиву private можно использовать для защиты от других директив, которые непреднамеренно могут сделать аутентифицированные ответы доступными общим кэшам.
public
Директива ответов public означает следующее:
общий кэш может хранить и повторно использовать ответ на запрос, содержащий заголовок
Authorization, что ему обычно запрещено делать; иприватный кэш может хранить ответ, даже если ответ эвристически не должен был бы кэшироваться.
s-maxage=
Директива ответов s-maxage=<number> аналогична max-age, но касается только общих кэшей. ☞ RFC 9111 § 5.2.2.10.
Также эта директива содержит в себе семантику директивы ответов proxy‑revalidate в том смысле, что общий кэш не должен использовать устаревший ответ, пока он не будет успешно валидирован исходным сервером.
Побочные эффекты: s-maxage — это одна из тех директив наряду с must-revalidate и public, которые позволяют общим кэшам хранить их повторно использовать ответ на запрос, содержащий заголовок Authorization, что обычно им делать запрещено.
proxy-revalidate
Директива ответов proxy-revalidate аналогична must-revalidate, но касается только общих кэшей. ☞ RFC 9111 § 5.2.2.8.
no-transform
Директива ответов no-transform указывает, что промежуточные звенья вне зависимости от того, реализуют они кэш или нет, не должны преобразовывать содержимое ответа, например, оптимизировать изображения или сжимать таблицы стилей и скрипты. ☞ RFC 9111 § 5.2.2.6.
stale-while-revalidate=
Директива ответов stale-while-revalidate определена в RFC 5861: HTTP Cache-Control Extensions for Stale Content (2010). Она указывает, что кэш может использовать кэшированный ответ, если его срок свежести не превышен больше, чем на заданное количество секунд.
Когда наличие этой директивы приводит к передаче устаревшего ответа, кэш должен запустить фоновую повторную валидацию ответа.
Автор RFC Марк Ноттингем написал обоснование этой директивы.
stale-if-error=
Директива ответов stale-if-error, тоже определённая в RFC 5861, указывает, что кэш может использовать кэшированный ответ, если срок его свежести не превышен больше, чем на заданное количество секунд, в случае, когда попытка валидации устаревшего запроса приводит к ошибке.
Судя по HTTP Caching Tests, эта директива имеет не очень хорошую поддержку.
Директивы запросов Cache-Control
Мы, веб-разработчики, чаще всего имеем дело с Cache-Control в HTTP-ответах, но этот заголовок может присутствовать и в HTTP-запросах. Браузеры, например, используют их, когда пользователь обновляет страницу.
При использовании в HTTP-запросах директивы Cache-Control выражают предпочтения клиента относительно свежести или срока существования ответа. Кэши согласовывают эти запросы с директивами ответов Cache-Control своих кэшированных ответов.
Примечание: опция
cacheметодаfetch()имеет отдельное множество значений, сопоставляемых с директивами запросовCache-Control, но сопоставление не всегда оказывается интуитивно понятным. Например,cache: 'no-cache'соответствуетCache-Control: max-age=0. Если вам любопытно, то сопоставления определены в спецификации. Также всегда можно задать заголовкиCache-Controlнапрямую при помощи опцииheaders.
max-age=
Директива запросов max-age указывает, что клиенту нужен свежий ответ, срок жизни которого меньше или равен заданному количеству секунд. В сочетании этой директивы с max-stale клиент принимает некоторые устаревшие ответы. ☞ RFC 9111 § 5.2.1.1.
max-stale=
Директива запросов max-stale указывает, что клиент принимает устаревшие ответы, превысившие его срок свежести не больше, чем на заданное количество секунд. При использовании без аргумента max-stale указывает, что клиент принимает любые устаревшие ответы. ☞ RFC 9111 § 5.2.1.2
min-fresh=
Директива запросов min-fresh указывает, что клиент предпочитает ответы, у которых осталось как минимум заданное количество секунд свежести. ☞ RFC 9111 § 5.2.1.3
no-cache
Директива запросов no-cache указывает, что клиент предпочитает, чтобы кэши не использовали сохр��нённые ответы без успешной валидации исходным сервером. ☞ RFC 9111 § 5.2.1.4
no-store
Директива запросов no-store указывает, что кэш не должен хранить никакую часть запроса и ответа на него. У неё есть те же особенности, что и у соответствующей директивы ответов. ☞ RFC 9111 § 5.2.1.5
Если кэш передаёт этот запрос с ответом, который был сохранён ранее, то директива запросов no-store не вынуждает кэш удалить ответ после его передачи.
no-transform
Директива запросов no-transform указывает, что клиент просит промежуточные звенья не преобразовывать содержимое. ☞ RFC 9111 § 5.2.1.6
only-if-cached
Директива запросов only-if-cached указывает, что клиенту требуется только сохранённый ответ. Кэши должны отвечать или сохранённым ответом, соответствующим всем остальным ограничениям, или кодом статуса HTTP 504 Gateway Timeout. ☞ RFC 9111 § 5.2.1.7
stale-if-error=
Аналогично соответствующей директиве ответов, директива запросов stale-if-error указывает, что клиент принимает устаревшие запросы, срок свежести которых превышен не больше, чем на заданное количество секунд, если попытка валидации привела к серверной ошибке.
Браузерные механизмы обновления
У браузеров обычно есть два механизма обновления:
мягкие перезагрузки, вызываемые кнопкой «Обновить», соответствующим пунктом меню и горячими клавишами, а также жестом «потянуть, чтобы обновить»; они предназначены для получения обновлённого вида страницы, например, чтобы просмотреть новые посты в ленте соцсетей.
жёсткие перезагрузки, вызываемые с клавишей-модификатором; они полностью игнорируют кэш и предназначены для исправления прерванных загрузок, устаревших кэшированных ответов и других поломанных состояний.
Ниже я расскажу, как реализуют это поведение некоторые браузеры в macOS.
Мягкие перезагрузки
Запускаются Ctrl + R в Windows/Linux и Command + R в macOS.
Firefox запускает условный запрос повторной валидации кэшированного запроса основного ресурса (файла HTML). Субресурсы (таблицы стилей, скрипты и изображения) перезагружаются обычным образом согласно своим директивам кэша.
Chrome ведёт себя похожим образом; отличие заключается в том, что запрос валидации основного ресурса также включает директиву Cache-Control: max-age=0 (которая не может нанести вреда).
Вместо повторной валидации кэшированного ответа Safari выполняет безусловный запрос основного ресурса, а затем обычным образом загружает субресурсы.
Жёсткие перезагрузки
Запускаются Ctrl + Shift + R в Windows/Linux и Command + Shift + R в macOS, за исключением Safari, где используются Command + Option + R. (Если вы уже автоматически использовали заученные горячие клавиши в Safari, то знаете, что они вместо этого открывают режим «Читатель»)
При жёсткой перезагрузке все три браузера выполняют безусловные запросы с директивой Cache-Control: no-cache для страницы HTML и её субресурсов.
Любопытно, что после выполнения жёсткой перезагрузки в Safari последующие мягкие перезагрузки для получения основного ресурса по-прежнему будут использовать директиву запросов Cache-Control: no-cache; вероятно, это непреднамеренное, но вполне безвредное поведение.
Директива ответов immutable
Обновление веб-страниц не всегда работало так. Раньше при выполнении мягкой перезагрузки все субресурсы повторно валидировались с основным ресурсом, по сути, освежая кэш для текущей страницы.
Примерно в 2015 году, когда пользователь обновлял страницу ленты браузерной кнопкой «Обновить», в Facebook начало появляться множество ответов HTTP 304 Not Modified для таких долгоживущих ресурсов, как скрипты и таблицы стилей.
Для решения этой проблемы Патрик Макманус из Mozilla предложил использовать директиву ответов immutable, которая позже превратилась в RFC 8246: HTTP Immutable Responses.
Эта директива указывает, что исходный сервер не будет обновлять ресурс в течение срока свежести кэшированного ответа, поэтому кэш не должен передавать условные запросы для ответов, которые остаются свежими, когда пользователь обновляет страницу, если только пользователь очень сильно не захочет получить обновлённый ответ (то есть не выполнит жёсткую перезагрузку).
Примерно в то время, когда поддержка immutable появилась в Firefox 49 и Facebook начал её успешно использовать, в Chrome добавили новый способ выполнения перезагрузок, решающий проблему без добавления новых директив: вместо повторной валидации всего при мягкой перезагрузке он просто повторно валидировал основной ресурс и обычным образом загружал субресурсы. Вскоре после этого на новую политику перезагрузки перешёл Safari [Webkit#169756], а Firefox сделал это в Firefox 100.
Из-за этого директива immutable оказалась в неудобном положении. В Safari добавили её поддержку [Webkit#167497], но представителей Chrome не убедило то, что она обеспечивает существенные преимущества по сравнению с имеющимся поведением перезагрузки [Chromium#41253661].
Кэширование ответов на аутентифицированные запросы
Один из самых запутанных аспектов HTTP-кэширования — это влияние различных директив ответов Cache-Control на то, как общие кэши обрабатывают ответы на запросы, содержащие заголовок Authorization, обозначающий привязку к одному пользователю.
Согласно RFC 9111 § 3.5, общим кэшам не разрешается хранить такие ответы, если только ответ не содержит поле Cache-Control с директивой ответа, позволяющей хранить его в общем кэше, и для этого ответа кэш подчиняется требованиям такой директивы.
Вот три директивы, позволяющие общим кэшам хранить аутентифицированные ответы; перед их применением следует всё тщательно продумать:
Директива private наоборот запрещает всем другим директивам хранить аутентифицированные ответы в общих кэшах.
Заключение
Я написал эту статью, чтобы самому разобраться в смысле различных директив кэширования, их пересечениях и взаимодействиях. В ней я рассмотрел только основные принципы, не углубляясь в более тёмные уголки семантики. Я изучал эту тему с «чистым кэшем», в основном пользуясь нормативными справочными документами (RFC 9111 и его дополнениями), а также различными руководствами из разных эпох:
Caching Tutorial for Web Authors and Webmasters (1998 год и далее) Марка Ноттингема;
Caching best practices & max-age gotchas (2016 год) Джейка Арчибальда;
Prevent unnecessary network requests with the HTTP Cache (2018 год) Ильи Григорика и Джеффа Посника;
Cache control for civilians (2019–2025 гг.) и Why Do We Have a Cache-Control Request Header? (2025 год) Гарри Робертса;
Cache-Control Recommendations (2021 год) Эйприл Кинг;
Web Caching на MDN.
Эти руководства любопытны тем, что их рекомендации не просто кодируют интерпретации спецификаций, но и содержат меры защиты от несоответствующих требованиям или устаревших браузерных кэшей и промежуточных звеньев.
В свете недавнего прогресса было бы здорово разобраться, в какой степени улучшилась ситуация и от каких мер защиты можно отказаться. Похоже, неплохим ресурсом для оценки ситуации могут быть HTTP Caching Tests.
