Spec-Zone.ru › HTTP

Кэширование

Кэширование HTTP

Кэш HTTP сохраняет ответ, связанный с запросом, и повторно использует сохранённый ответ для последующих запросов.

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

Кроме того, когда ответ можно использовать повторно, исходному серверу не нужно обрабатывать запрос — то есть анализировать и маршрутизировать его, восстанавливать сеанс на основе cookie, запрашивать результаты в БД или выполнять шаблонизатор. Это снижает нагрузку на сервер.

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

Типы кэшей

В спецификации Кэширование HTTP определены два основных типа кэшей: частные кэши и общие кэши.

Частные кэши

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

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

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

Cache-Control: private

Персонализированное содержимое обычно определяется с помощью cookie, но наличие cookie не всегда означает, что ответ является частным, поэтому одна лишь cookie не делает ответ частным.

Общий кэш

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

Прокси-кэши

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

Для попытки обойти ограничения «старых и не обновляемых прокси-кэшей», которые не понимают современные директивы спецификации кэширования HTTP, такие как no-store, используются заголовки на все случаи жизни, например следующие.

Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate

Однако в последние годы HTTPS получил широкое распространение, а обмен данными между клиентом и сервером стал шифроваться. Поэтому прокси-кэши на пути передачи данных во многих случаях могут лишь туннелировать ответ, но не работать как кэш. В этом сценарии не нужно беспокоиться об устаревших реализациях прокси-кэшей, которые даже не могут видеть ответ.

С другой стороны, если прокси-мост TLS расшифровывает весь обмен данными по принципу «человек посередине», устанавливая на ПК сертификат от CA (центра сертификации), которым управляет организация, и осуществляет контроль доступа и т. д., он может видеть содержимое ответа и кэшировать его. Однако в последние годы широкое распространение получил CT (прозрачность сертификатов), а некоторые браузеры разрешают использовать только сертификаты, выданные с SCT (меткой времени подписанного сертификата). Поэтому для этого метода требуется применение корпоративной политики. В такой контролируемой среде не нужно беспокоиться о том, что прокси-кэш «устарел и не обновляется».

Управляемые кэши

Управляемые кэши явно развёртываются разработчиками сервисов, чтобы разгрузить исходный сервер и эффективно доставлять содержимое. К ним относятся обратные прокси, CDN и service workers в сочетании с Cache API.

Характеристики управляемых кэшей зависят от развёрнутого продукта. В большинстве случаев поведением кэша можно управлять с помощью заголовка Cache-Control и собственных файлов конфигурации или панелей управления.

Например, спецификация кэширования HTTP по существу не определяет способ явного удаления кэша, но в управляемом кэше сохранённый ответ можно удалить в любой момент с помощью операций на панели управления, вызовов API, перезапусков и т. д. Это позволяет применять более проактивную стратегию кэширования.

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

Cache-Control: no-store

Например, Varnish Cache использует логику VCL (Varnish Configuration Language, разновидность DSL) для управления хранением кэша, а service workers в сочетании с Cache API позволяют создавать такую логику на JavaScript.

Это означает, что если управляемый кэш намеренно игнорирует директиву no-store, не нужно считать его «несовместимым» со стандартом. Следует избегать заголовков на все случаи жизни, внимательно изучить документацию используемого механизма управляемого кэширования и убедиться, что вы правильно управляете кэшем средствами, предоставляемыми выбранным механизмом.

Обратите внимание: некоторые CDN предоставляют собственные заголовки, действующие только для этой CDN (например, Surrogate-Control). В настоящее время ведётся работа над определением заголовка CDN-Cache-Control для их стандартизации.

Types of caches, including a private cache in the browser, a shared (proxy) cache, a reverse proxy cache, and a shared (managed) cache in a CDN, leading to the origin server's cache

Эвристическое кэширование

HTTP разработан так, чтобы кэшировать как можно больше, поэтому даже если не задана директива Cache-Control, ответы будут сохраняться и повторно использоваться при выполнении определённых условий. Это называется эвристическим кэшированием.

Рассмотрим, например, следующий ответ. В последний раз он обновлялся год назад.

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2021 22:22:22 GMT

<!doctype html>
…

Эвристически предполагается, что содержимое, которое не обновлялось целый год, не будет обновлено и в течение некоторого времени после этого. Поэтому клиент сохраняет этот ответ (несмотря на отсутствие max-age) и некоторое время повторно его использует. Продолжительность повторного использования зависит от реализации, но спецификация рекомендует установить её примерно равной 10% времени после сохранения (в данном случае — 0,1 года).

Эвристическое кэширование — это обходное решение, появившееся до широкого внедрения поддержки Cache-Control, и практически во всех ответах следует явно указывать заголовок Cache-Control.

Свежие и устаревшие ответы в зависимости от возраста

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

Критерий, по которому определяется, является ли ответ свежим или устаревшим, — это его возраст. В HTTP возраст — это время, прошедшее с момента создания ответа. Это похоже на TTL в других механизмах кэширования.

Рассмотрим следующий пример ответа (604800 секунд — это одна неделя):

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800

<!doctype html>
…

Кэш, сохранивший пример ответа, вычисляет время, прошедшее с момента его создания, и использует результат как возраст ответа.

В примере ответа max-age означает следующее:

  • Если возраст ответа меньше одной недели, ответ считается свежим.
  • Если возраст ответа больше одной недели, ответ считается устаревшим.

Пока сохранённый ответ остаётся свежим, он будет использоваться для выполнения запросов клиентов.

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

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800
Age: 86400

<!doctype html>
…

Клиент, получивший этот ответ, будет считать его свежим в течение оставшихся 518400 секунд — разницы между max-age ответа и Age.

Expires или max-age

В HTTP/1.0 свежесть раньше задавалась заголовком Expires.

Заголовок Expires задаёт время жизни кэша с помощью конкретного времени, а не указания прошедшего времени.

Expires: Tue, 28 Feb 2022 22:22:22 GMT

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

Если доступны оба заголовка — Expires и Cache-Control: max-age, — приоритет, согласно определению, отдаётся max-age. Поэтому теперь, когда HTTP/1.1 широко используется, указывать Expires необязательно.

Vary

Ответы различаются между собой главным образом по URL:

URL Тело ответа
https://example.com/index.html <!doctype html>...
https://example.com/style.css body { ...
https://example.com/script.js function main () { ...

Но содержимое ответов не всегда одинаково, даже если у них один и тот же URL. В частности, при согласовании содержимого ответ сервера может зависеть от значений заголовков запроса Accept, Accept-Language и Accept-Encoding.

Например, если содержимое на английском языке возвращено с заголовком Accept-Language: en и кэшировано, нежелательно затем использовать этот сохранённый ответ повторно для запросов с заголовком запроса Accept-Language: ja. В этом случае можно разделить кэшируемые ответы по языку, добавив Accept-Language к значению заголовка Vary.

Vary: Accept-Language

В результате ключ кэша будет формироваться на основе URL ответа и заголовка запроса Accept-Language, а не только URL ответа.

URL Accept-Language Тело ответа
https://example.com/index.html ja-JP <!doctype html>...
https://example.com/index.html en-US <!doctype html>...
https://example.com/style.css ja-JP body { ...
https://example.com/script.js ja-JP function main () { ...

Кроме того, если вы оптимизируете содержимое (например, для адаптивного дизайна) в зависимости от пользовательского агента, может возникнуть желание включить User-Agent в значение заголовка Vary. Однако заголовок запроса User-Agent обычно имеет очень много вариантов, что значительно снижает вероятность повторного использования кэша. Поэтому, если возможно, лучше продумать способ изменять поведение на основе обнаружения возможностей, а не заголовка запроса User-Agent.

В приложениях, использующих cookie для защиты от повторного использования другими лицами кэшированного персонализированного содержимого, вместо указания cookie для Vary следует указать Cache-Control: private.

Проверка актуальности

Устаревшие ответы не отбрасываются немедленно. В HTTP есть механизм преобразования устаревшего ответа в свежий путём обращения к исходному серверу. Он называется проверкой актуальности, или иногда повторной проверкой актуальности.

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

If-Modified-Since

Следующий ответ был создан в 22:22:22 и имеет max-age продолжительностью 1 час, поэтому он остаётся свежим до 23:22:22.

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

<!doctype html>
…

В 23:22:22 ответ становится устаревшим, и повторно использовать его из кэша нельзя. Поэтому в приведённом ниже примере клиент отправляет запрос с заголовком запроса If-Modified-Since, чтобы спросить сервер, были ли внесены изменения после указанного времени.

GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-Modified-Since: Tue, 22 Feb 2022 22:00:00 GMT

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

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

HTTP/1.1 304 Not Modified
Content-Type: text/html
Date: Tue, 22 Feb 2022 23:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

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

Сервер может получить время изменения из файловой системы операционной системы; это сравнительно просто сделать при выдаче статических файлов. Однако есть некоторые проблемы: например, формат времени сложен и его трудно анализировать, а распределённым серверам трудно синхронизировать время обновления файлов.

Чтобы решить эти проблемы, в качестве альтернативы был стандартизирован заголовок ответа ETag.

ETag/If-None-Match

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

Например, если для заголовка ETag используется хеш-значение, а хеш ресурса index.html равен 33a64df5, ответ будет выглядеть следующим образом:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
ETag: "33a64df5"
Cache-Control: max-age=3600

<!doctype html>
…

Если этот ответ устарел, клиент берёт значение заголовка ответа ETag из кэшированного ответа и помещает его в заголовок запроса If-None-Match, чтобы спросить сервер, был ли изменён ресурс:

GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-None-Match: "33a64df5"

Сервер вернёт 304 Not Modified, если определённое им значение заголовка ETag для запрошенного ресурса совпадает со значением If-None-Match в запросе.

Но если сервер определит, что запрошенному ресурсу теперь должно соответствовать другое значение ETag, он вместо этого вернёт 200 OK и последнюю версию ресурса.

Примечание: RFC9110 рекомендует серверам, если возможно, отправлять и ETag, и Last-Modified для ответа 200. Во время повторной проверки кэша, если присутствуют оба заголовка — If-Modified-Since и If-None-Match, — для валидатора приоритет имеет If-None-Match. Если вы рассматриваете только кэширование, вам может показаться, что Last-Modified не нужен. Однако Last-Modified полезен не только для кэширования: это стандартный заголовок HTTP, который также используется системами управления содержимым (CMS) для отображения времени последнего изменения, поисковыми роботами — для корректировки частоты обхода, а также в других целях. Поэтому с учётом всей экосистемы HTTP лучше предоставлять и ETag, и Last-Modified.

Принудительная проверка актуальности

Если вы не хотите повторно использовать ответ, а хотите всегда получать с сервера последнее содержимое, можно использовать директиву no-cache, чтобы принудительно выполнить проверку актуальности.

Добавив Cache-Control: no-cache к ответу вместе с Last-Modified и ETag, как показано ниже, клиент получит ответ 200 OK, если запрошенный ресурс обновился, или ответ 304 Not Modified, если ресурс не обновился.

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
ETag: "deadbeef"
Cache-Control: no-cache

<!doctype html>
…

Часто утверждается, что сочетание max-age=0 и must-revalidate имеет тот же смысл, что и no-cache.

Cache-Control: max-age=0, must-revalidate

max-age=0 означает, что ответ немедленно становится устаревшим, а must-revalidate означает, что после устаревания его нельзя повторно использовать без проверки актуальности. Поэтому в сочетании эти директивы, как кажется, имеют ту же семантику, что и no-cache.

Однако такое использование max-age=0 — пережиток тех времён, когда многие реализации до HTTP/1.1 не умели обрабатывать директиву no-cache. Чтобы обойти это ограничение, использовалась директива max-age=0.

Но теперь, когда серверы, соответствующие HTTP/1.1, получили широкое распространение, нет причин использовать сочетание max-age=0 и must-revalidate — вместо него следует просто использовать no-cache.

Не кэшировать

Директива no-cache не запрещает сохранять ответы, а лишь запрещает повторно использовать их без проверки актуальности.

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

Cache-Control: no-store

Однако на практике требование «не кэшировать» обычно подразумевает следующее:

  • По соображениям конфиденциальности ответ не должен сохраняться никем, кроме конкретного клиента.
  • Всегда нужно предоставлять актуальную информацию.
  • Неизвестно, что может произойти в устаревших реализациях.

В таких обстоятельствах директива no-store не всегда будет наиболее подходящей.

В следующих разделах эти обстоятельства рассматриваются подробнее.

Не предоставлять другим

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

В этом случае директива private позволит сохранить персонализированный ответ только у конкретного клиента, не раскрывая его другим пользователям кэша.

Cache-Control: private

В этом случае, даже если указана no-store, необходимо также указать private.

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

Директива no-store не позволяет сохранять ответ, но не удаляет уже сохранённый ответ для того же URL.

Иными словами, если для определённого URL уже сохранён старый ответ, возврат no-store не помешает повторно использовать этот ответ.

Однако директива no-cache заставит клиента отправить запрос для проверки актуальности перед повторным использованием любого сохранённого ответа.

Cache-Control: no-cache

Если сервер не поддерживает условные запросы, можно заставить клиента обращаться к серверу при каждом запросе и всегда получать последний ответ с помощью 200 OK.

Работа с устаревшими реализациями

В качестве обходного решения для устаревших реализаций, игнорирующих no-store, можно встретить такие заголовки на все случаи жизни, как следующий.

Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate

Для работы с такими устаревшими реализациями рекомендуется использовать no-cache. Нет ничего плохого в том, чтобы сразу указать no-cache, поскольку сервер всегда будет получать запрос.

Если вас беспокоит общий кэш, можно также добавить private, чтобы предотвратить непреднамеренное кэширование:

Cache-Control: no-cache, private

От чего вы отказываетесь, используя no-store

Возможно, вам кажется, что no-store — правильный способ отказаться от кэширования.

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

Поэтому, чтобы воспользоваться всеми возможностями веб-платформы, лучше применять no-cache в сочетании с private.

Перезагрузка и принудительная перезагрузка

Проверка актуальности может выполняться как для запросов, так и для ответов.

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

Перезагрузка

Чтобы восстановить работу после повреждения окна или обновить ресурс до последней версии, браузеры предоставляют пользователям функцию перезагрузки.

Упрощённый пример HTTP-запроса, отправляемого при перезагрузке страницы в браузере:

GET / HTTP/1.1
Host: example.com
Cache-Control: max-age=0
If-None-Match: "deadbeef"
If-Modified-Since: Tue, 22 Feb 2022 20:20:20 GMT

(Запросы Chrome, Edge и Firefox очень похожи на приведённый выше; запросы Safari немного отличаются.)

Директива max-age=0 в запросе означает «повторно использовать ответы с возрастом не больше 0», поэтому сохранённые промежуточным кэшем ответы фактически не используются повторно.

В результате актуальность запроса проверяется с помощью If-None-Match и If-Modified-Since.

Такое поведение также определено в стандарте Fetch. Его можно воспроизвести на JavaScript, вызвав fetch() с режимом кэширования no-cache (обратите внимание, что для этого случая не подходит режим reload):

// Note: "reload" is not the right mode for a normal reload; "no-cache" is
fetch("/", { cache: "no-cache" });

Принудительная перезагрузка

Во время перезагрузки браузеры используют max-age=0 для обратной совместимости, поскольку многие устаревшие реализации до HTTP/1.1 не понимали no-cache. Но теперь no-cache подходит для этого случая, а принудительная перезагрузка — это дополнительный способ обойти сохранённые в кэше ответы.

HTTP-запрос при принудительной перезагрузке страницы в браузере выглядит следующим образом:

GET / HTTP/1.1
Host: example.com
Pragma: no-cache
Cache-Control: no-cache

(Запросы Chrome, Edge и Firefox очень похожи на приведённый выше; запросы Safari немного отличаются.)

Поскольку это не условный запрос с no-cache, можно быть уверенным, что от исходного сервера будет получен 200 OK.

Такое поведение также определено в стандарте Fetch. Его можно воспроизвести на JavaScript, вызвав fetch() с режимом кэширования reload (обратите внимание: это не force-reload):

// Note: "reload" — rather than "no-cache" — is the right mode for a "force reload"
fetch("/", { cache: "reload" });

Как избежать повторной проверки актуальности

Для содержимого, которое никогда не меняется, следует задавать длительный max-age, используя версионирование кэша — то есть добавляя номер версии, хеш и т. п. в URL запроса.

Однако при перезагрузке пользователем отправляется запрос для повторной проверки актуальности, даже если сервер знает, что содержимое неизменно.

Чтобы этого избежать, можно использовать директиву immutable, явно указывающую, что проверка актуальности не требуется, поскольку содержимое никогда не меняется.

Cache-Control: max-age=31536000, immutable

Это предотвращает ненужную повторную проверку актуальности при перезагрузке.

Обратите внимание: вместо реализации этой директивы Chrome изменил свою реализацию, чтобы при перезагрузке не выполнять повторную проверку актуальности для подресурсов.

Удаление сохранённых ответов

Удалить ответы с промежуточного сервера, сохранённые с длительным значением max-age, невозможно.

Представьте, что был сохранён следующий ответ от https://example.com/.

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: max-age=31536000

<!doctype html>
…

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

Один из методов, упомянутых в спецификации, — отправить запрос к тому же URL с небезопасным методом, например POST, но многим клиентам это сложно сделать.

Заголовок Clear-Site-Data: cache и значение его директивы можно использовать для очистки кэша браузера, но на промежуточные кэши они не влияют. В противном случае ответы останутся в кэше браузера до истечения срока действия max-age, если только пользователь вручную не выполнит перезагрузку, принудительную перезагрузку или очистку истории.

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

Объединение запросов

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

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

Объединение запросов происходит, когда запросы поступают одновременно, поэтому даже если в ответе указано max-age=0 или no-cache, он будет использован повторно.

Если ответ персонализирован для конкретного пользователя и вы не хотите, чтобы он передавался другим при объединении запросов, следует добавить директиву private:

Request collapse shown as multiple clients sending GET requests and a cache consolidating them into one GET to the origin. The origin server responds with a 200 OK that the cache shares back to all clients.

Распространённые схемы кэширования

В спецификации Cache-Control содержится множество директив, и понять их все может быть сложно. Однако для большинства веб-сайтов достаточно сочетания нескольких типовых схем.

В этом разделе описаны распространённые схемы проектирования кэшей.

Настройки по умолчанию

Как упоминалось выше, поведение по умолчанию при кэшировании (то есть для ответа без Cache-Control) — это не просто «не кэшировать», а неявное кэширование в соответствии с так называемым «эвристическим кэшированием».

Чтобы избежать такого эвристического кэширования, предпочтительно явно указывать для всех ответов заголовок Cache-Control по умолчанию.

Чтобы по умолчанию всегда передавались последние версии ресурсов, обычно в значение Cache-Control по умолчанию включают no-cache:

Cache-Control: no-cache

Кроме того, если служба использует файлы cookie или другие методы входа и персонализирует содержимое для каждого пользователя, необходимо также указывать private, чтобы предотвратить его совместное использование с другими пользователями:

Cache-Control: no-cache, private

Обход кэша

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

Рассмотрим следующий HTML-код:

<script src="bundle.js"></script>
<link rel="stylesheet" href="build.css" />
<body>
  hello
</body>

В современной веб-разработке ресурсы JavaScript и CSS часто обновляются по мере разработки. Кроме того, если версии ресурсов JavaScript и CSS, используемые клиентом, не синхронизированы, отображение будет нарушено.

Поэтому приведённый выше HTML-код затрудняет кэширование bundle.js и build.css с помощью max-age.

Таким образом, JavaScript и CSS можно предоставлять по URL, включающим изменяющуюся часть на основе номера версии или хеш-значения. Ниже показаны некоторые способы сделать это.

# version in filename
bundle.v123.js

# version in query
bundle.js?v=123

# hash in filename
bundle.YsAIAAAA-QG4G6kCMAMBAAAAAAAoK.js

# hash in query
bundle.js?v=YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

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

<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
  hello
</body>

При таком подходе ресурсы JavaScript и CSS можно кэшировать в течение длительного времени. Какое значение следует задать для max-age? Ответ на этот вопрос даёт спецификация QPACK.

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

Ниже показаны некоторые часто используемые значения заголовков кэширования.

36 cache-control max-age=0
37 cache-control max-age=604800
38 cache-control max-age=2592000
39 cache-control no-cache
40 cache-control no-store
41 cache-control public, max-age=31536000

Если выбрать один из этих пронумерованных вариантов, значения можно будет сжать до 1 байта при передаче по HTTP3.

Числа 37, 38 и 41 соответствуют периодам в одну неделю, один месяц и один год.

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

Обратите внимание: у числа 41 самое длительное значение max-age (1 год), но оно используется с public.

Значение public позволяет сохранять ответ, даже если присутствует заголовок Authorization.

Примечание: Директиву public следует использовать только в том случае, если необходимо сохранять ответ при установленном заголовке Authorization. В остальных случаях она не требуется, поскольку ответ будет сохранён в общем кэше, если задано max-age.

Таким образом, если ответ персонализирован с помощью базовой аутентификации, наличие public может вызвать проблемы. Если вас это беспокоит, можно выбрать следующее по длительности значение — 38 (1 месяц).

# response for bundle.v123.js

# If you never personalize responses via Authorization
Cache-Control: public, max-age=31536000

# If you can't be certain
Cache-Control: max-age=2592000

Проверка актуальности

Не забудьте задать заголовки Last-Modified и ETag, чтобы при повторной загрузке не передавать ресурс заново. Для заранее собранных статических файлов эти заголовки легко сгенерировать.

Значением ETag в этом случае может быть хеш файла.

# response for bundle.v123.js
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: "YsAIAAAA-QG4G6kCMAMBAAAAAAAoK"

Кроме того, можно добавить immutable, чтобы избежать проверки актуальности при повторной загрузке.

Итоговый вариант показан ниже.

# bundle.v123.js
HTTP/1.1 200 OK
Content-Type: text/javascript
Content-Length: 1024
Cache-Control: public, max-age=31536000, immutable
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: "YsAIAAAA-QG4G6kCMAMBAAAAAAAoK"

Обход кэша — это способ обеспечить длительное кэширование ответа, меняя URL при изменении содержимого. Этот способ можно применять ко всем подресурсам, например к изображениям.

Примечание: При оценке использования immutable и QPACK: если вы опасаетесь, что immutable изменит предопределённое значение, предоставляемое QPACK, учтите, что в этом случае часть immutable можно закодировать отдельно, разделив значение Cache-Control на две строки. Однако это зависит от алгоритма кодирования, используемого конкретной реализацией QPACK.

Cache-Control: public, max-age=31536000
Cache-Control: immutable

Основные ресурсы

В отличие от подресурсов, для основных ресурсов нельзя использовать обход кэша, поскольку их URL нельзя изменить так же, как URL подресурсов.

Если сохранить приведённый ниже HTML-код, последнюю версию нельзя будет отобразить, даже если содержимое обновится на стороне сервера.

<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
  hello
</body>

В этом случае подойдёт no-cache, а не no-store, поскольку мы не хотим сохранять HTML-код, а хотим, чтобы он всегда был актуальным.

Кроме того, добавление Last-Modified и ETag позволит клиентам отправлять условные запросы. Если HTML-код не изменился, можно вернуть 304 Not Modified:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: no-cache
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: "AAPuIbAOdvAGEETbgAAAAAAABAAE"

Эта настройка подходит для неперсонализированного HTML-кода, но для ответа, персонализируемого с помощью файлов cookie — например, после входа в систему, — не забудьте также указать private:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: no-cache, private
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: "AAPuIbAOdvAGEETbgAAAAAAABAAE"
Set-Cookie: __Host-SID=AHNtAyt3fvJrUL5g5tnGwER; Secure; Path=/; HttpOnly

То же самое можно использовать для favicon.ico, manifest.json, .well-known и конечных точек API, URL которых нельзя изменить с помощью обхода кэша.

Большинство веб-контента можно охватить сочетанием двух описанных выше подходов.

Подробнее об управляемых кэшах

С помощью описанного в предыдущих разделах метода подресурсы можно кэшировать в течение длительного времени, используя обход кэша, но основные ресурсы (обычно HTML-документы) — нельзя.

Кэшировать основные ресурсы сложно, поскольку одних стандартных директив из спецификации кэширования HTTP недостаточно, чтобы активно удалять содержимое кэша при обновлении содержимого на сервере.

Однако это возможно, если использовать управляемый кэш, например CDN или service worker.

Например, CDN, позволяющая очищать кэш с помощью API или панели управления, даст возможность применить более агрессивную стратегию кэширования: сохранять основной ресурс и явно очищать соответствующий кэш только при обновлении на сервере.

Service worker может делать то же самое, если он способен удалять содержимое Cache API при обновлении на сервере.

Дополнительные сведения см. в документации к вашей CDN, а также в документации по service worker.

См. также

  • RFC 9111: Протокол передачи гипертекста (HTTP/1.1): кэширование
  • Руководство по кэшированию — Марк Ноттингем

© 2005–2025 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API