HTTP-кеширование
Из всех байтов, суетящихся в Интернете в любой момент, подавляющее большинство из них являются статическими или вряд ли изменятся со временем. Изображения, видео и шрифты все попадают в эту категорию, и к этим ресурсам можно отнести множество проблем производительности современного интернета.
Проблема не обязательно в том, что эти ресурсы велики или что они неоптимизированы, а в том, что многим веб-приложениям нужно извлекать их при каждом обновлении страницы. Если бы пользователи могли тратить меньше времени на загрузку вашего великолепного изображения героя Unsplash и больше времени на рендеринг HTML и анализ JavaScript, они бы столкнулись с более быстрым приложением.
К счастью, HTTP предлагает мощное решение: кэширование . В этом посте объясняется, как HTTP может дать указание браузерам повторно использовать дорогие ресурсы, экономя пропускную способность и время. Мы начнем с обзора концепций кэширования, а затем опишем, как эволюционировал механизм кэширования HTTP между HTTP / 1.0 и HTTP / 1.1.
Основы кэширования
В своей простейшей форме кеш просто позволяет системе удерживать ресурс, если он знает, что указанный ресурс вряд ли изменится в течение определенного периода времени. До истечения этого периода времени ресурс считается свежим , а после этого считается устаревшим . Эти два термина имеют ключевое значение для понимания кэширования.
Идеальный кеш позволил бы клиенту, такому как браузер, как можно дольше удерживать ресурс, прежде чем выбросить его для более новой версии, когда это необходимо. Легко сказать, но это одна из «двух сложных проблем» в информатике.
Для того чтобы кэш функционировал, вам нужен способ указать, как долго ресурс будет свежим, что является невыполнимой задачей для сервера, поскольку он не может с абсолютной уверенностью предсказать, когда (если вообще когда-либо) будет обновлен ресурс. Этими знаниями обладаете только вы, как разработчик.
Как это сделать? HTTP предлагает два основных средства, первое из которых мы сейчас рассмотрим.
Expires и истоки кеширования HTTP
Expires
был введен в HTTP / 1.0 , и, вместе с Pragma
, Last-Modified
и If-Modified-Since
, первую систему кэширования , состоящей протокола HTTP. Это самый простой из имеющихся в вашем распоряжении заголовков кэширования HTTP, указывающий дату, когда данный ресурс устареет:
GET https://www.example.com/image.jpeg HTTP/1.1 Status: 200 Content-Type: image/jpeg Last-Modified: Fri, 12 Apr 2019 08:00:00 GMT Expires: Sun, 14 Apr 2019 08:00:00 GMT
По истечении указанного срока Expires
браузер попытается повторно получить указанный ресурс. До этого браузер может свободно удерживать ресурс и использовать его по своему усмотрению.
Повторная проверка с Last-Modified и If-Modified-Since
Помните, как мы упоминали, что идеальный кеш будет загружать новый ресурс только тогда, когда он будет уверен на 100%? Один из способов добиться этого - позволить браузерам опрашивать серверы в зависимости от того, действительно ли такой ресурс доступен. Но как браузер может указать, какая версия ресурса у него в настоящее время?
Введите If-Modified-Since. Расширяя наш пример выше, представьте, что браузер хотел получить image.jpeg 15 апреля, на следующий день после даты, указанной в Expires заголовке. Вы заметите, что приведенный выше фрагмент кода содержит Last-Modified заголовок, который указывает, когда сервер в последний раз полагал, что изображение было обновлено.
Учитывая, что браузер уже имеет image.jpeg в своем кэше, он может сообщить серверу, что у него уже есть копия изображения, которое было изменено в последний раз 12 апреля:
GET https://www.example.com/image.jpeg HTTP/1.1 If-Modified-Since: Fri, 12 Apr 2019 08:00:00 GMT
Если изображение изменилось, сервер просто ответит полным ответом, содержащим новое изображение. В противном случае он может ответить 304 Not Modified
:
GET https://www.example.com/image.jpeg HTTP/1.1 Status: 304 Expires: Sun, 14 Apr 2019 08:00:00 GMT Last-Modified: Fri, 12 Apr 2019 08:00:00 GMT
После получения такого ответа браузер может выпустить свою кэшированную копию. Этот результат является выигрышным как для сервера, так и для клиента: сервер гарантирует, что используется самая последняя версия ресурса, и клиенту не нужно повторно загружать образ.
cache-control
и эволюция HTTP-кеширования
Ограничения Expires
привели к появлению cache-control
в HTTP/1.1, что значительно увеличило гибкость, с которой разработчики могли кэшировать ресурсы. Вместо того, чтобы строго полагаться на даты, cache-control
принимает ряд директив, пару из которых мы обсудим сейчас, а остальные сворачиваются в дискуссии о повторной проверке, безопасности и многом другом.
Директива max-age
Думайте о max-age
директиве как о более простой альтернативе Expires
. Если вы хотите указать, что ресурс истекает в течение одного дня, вы можете ответить следующим заголовком cache-control
:
GET https://www.example.com/image.jpeg HTTP/1.1 Status: 200 Cache-Control: max-age=86400
Обратите внимание, что max-age
относится ко времени запроса, поэтому таймер начинает отсчитывать время, когда ресурс входит в кеш. Вы можете спросить: «Зачем переходить на секунды по датам?»
У Марка Ноттингема есть хорошее объяснение , и он подчеркивает простоту, работы с max-age
. Учтите следующее:
GET https://www.example.com/image.jpeg HTTP/1.1 Status: 200 Expires: Mon, 15 Apr 2019 08:00:00 GMT Cache-Control: max-age=86400
Мало того, что может быть трудно сопоставить дату Expires
с вашим местным часовым поясом, многие реализации сервера просто испортили формат даты, что привело к путанице. max-age
будучи простым целым числом, представляющим секунды с момента генерации ответа, гораздо проще использовать.
Самая длинная продолжительность, которую может поддерживать директива max-age
, составляет год, который удовлетворяет большинству вариантов использования. Но если вы сообщаете браузеру, что определенный ресурс никогда не изменится, вы также можете использовать более новую директиву immutable
, которая выполняет именно это. Имейте в виду, что его применение не является полностью совместимым в разных браузерах, поэтому стоит добавить ее вместе с ним max-age
.
Повторная проверка с Etag
и If-None-Match
HTTP / 1.1 также представил новую стратегию переаттестации для дополнения If-Modified-Since
, сосредоточенную вокруг того, что называют «тегами сущностей».
Вы можете рассматривать теги сущностей как способ, с помощью которого серверы могут однозначно идентифицировать версию ресурса с помощью буквенно-цифрового идентификатора, указанного в заголовке ответа ETag
:
GET https://www.example.com/image.jpeg HTTP/1.1 Status: 200 Content-Type: image/jpeg ETag: abc
Если клиент хочет сообщить серверу, что у него есть конкретная версия ресурса, он предоставляет заголовок запроса If-None-Match
при следующем запросе ресурса:
GET https://www.example.com/image.jpeg HTTP/1.1 If-None-Match: abc
Если последняя версия ресурса не соответствует тегу сущности «abc», сервер ответит новой версией. В противном случае он ответит 304 Not Modified
.
GET https://www.example.com/image.jpeg HTTP/1.1 Status: 304 Content-Type: image/jpeg ETag: abc
Как вы, наверное, догадались, «abc» - это слишком простой идентификатор, который можно использовать для ресурса. Как бы вы гарантировали, что определенный тег сущности соответствует одной версии ресурса, а не другой?
Именно эта конкретная проблема приводит к тому, что популярные хранилища, такие как S3, используют алгоритмы хеширования, например, md5
для создания тегов сущностей. Используя теги сущностей, привязанные к фактическому дайджесту байтов, составляющих ресурсы, вы получаете идентификатор, уникальный для этого файла.
Вы можете указать директиву
must-revalidate
внутриcache-control
чтобы информировать клиентов о том, что они должны использовать механизм проверки, будь то теги сущностей илиIf-Modified-Since
, прежде чем выпускать устаревшую копию ресурса из кэша (в случае, если сервер недоступен).
Обеспечение конфиденциальности кэша с помощью private
и public
До этого момента мы фокусировались на кеше, который существует в вашем браузере, который кэширует ресурсы по мере их загрузки. Но реальность такова, что ресурсы часто проходят через один или несколько промежуточных или «общих» кэшей, прежде чем они попадают в ваш браузер. Это могут быть кеши, используемые вашим интернет-провайдером или корпоративным ИТ-отделом.
До того, как HTTPS получил широкое распространение, многие промежуточные кэши стали использовать ресурсы в случае, если другой пользователь может найти это полезным. Не нужно много воображения, чтобы увидеть, это может быть не так.
Чтобы смягчить это, в HTTP / 1.1 были введены директивы public
и private
в cache-control
, которые, хотя и несовершенны, позволяют указывать такие общие кэши, если вы не хотели, чтобы они хранили копию ресурса.
Эти директивы все еще полезны. Если вы используете компьютер с несколькими людьми, а ваш браузер совместно использует кеш между ними, существует вероятность, что он может поделиться с ними загруженными ресурсами. Если ресурс указывает директиву private
, то браузер должен сделать все возможное, чтобы гарантировать, что только вы, пользователь, который его загрузил, может использовать его повторно.
Подавление кэширования с помощью no-cache
и no-store
HTTP / 1.1 исправил недостаточность заголовка Pragma
в HTTP / 1.0 и предоставил веб-разработчикам средства, с помощью которых можно полностью отключить кэширование.
Первая директива, no-cache
заставляет кеш повторно проверять ресурс перед повторным использованием. В отличие от этого must-revalidate
, no-cache
означает, что браузер значительно обновляется во всех случаях, а не только тогда, когда ресурс устарел.
Вторая директива, no-store
это молоток: она сигнализирует, что ресурс ни в коем случае не должен входить в кеш.
Указание специфичных для запроса ограничений кэширования
Что если в качестве клиента вы захотите запросить ресурс, который будет обновлен хотя бы в течение определенного периода времени? Хорошая новость заключается в том, что cache-control
это не только инструмент для серверов, задающий ограничения кэширования для клиентов, но и доступный для клиентов, чтобы они могли получить ресурс, который соответствует определенным требованиям к кэшированию.
Директивы max-age
, no-cache
и no-store
все они могут быть использованы в запросах клиента. Их значение имеет тенденцию быть обратным тому, что это означает для клиента; например, указание заголовка max-age
в запросе говорит любым промежуточным серверам, что они не могут использовать кэшированные ответы старше, чем продолжительность, указанная в директиве.
В дополнение к указанным выше директивам, в cache-control
в вашем распоряжении четыре директивы только для запроса.
min-fresh
- позволяет клиентам запрашивать ресурсы, которые будут обновлены, по крайней мере, в течение определенного периода в секундах:
GET https://www.example.com/image.jpeg HTTP/1.1 Cache-Control: min-fresh=3600
Директива max-stale
сообщает любым промежуточным серверам кэша, что клиент готов принять несвежий ответ , который не был несвежим больше , чем определенный период времени в секундах:
GET https://www.example.com/image.jpeg HTTP/1.1 Cache-Control: max-stale=3600
Директива no-transform
говорит промежуточным серверам, что клиент не хочет какой - либо версии ресурса, указанного кэша , возможно, изменен. Это применимо в случаях, когда кэш, такой как Cloudflare, мог применять сжатие gzip
.
И, наконец, директива only-if-cached
сообщает промежуточным кэшам, что клиент хочет получить только кэшированный ответ, и что в противном случае ему не следует связываться с сервером для получения свежей копии. Если кеш не может удовлетворить запрос, он должен вернуть ответ 504 Gateway Timeout
.
Vary
и согласованные с сервером ответы
Наша последняя тема посвящена тому, как браузеры на самом деле идентифицируют ресурсы, и как согласование сервера влияет на ситуацию.
На высоком уровне кэш браузера действительно просто смотрит на URL и метод, хотя практически все кешируемые запросы являются запросами GET, практическая реальность такова, что URL, как правило, достаточно, чтобы браузеры идентифицировали ресурсы.
Это начинает разрушаться, когда мы понимаем, что два ответа, обслуживаемых для одного и того же URL, могут отличаться в зависимости от агента пользователя или использовать разные стратегии сжатия.
Для этого кэши обращают пристальное внимание на то, какие заголовки сервер использует для согласования соответствующего ответа, который передается клиенту через заголовок Vary
. Например, представьте, что мы сделали следующий запрос на изображение:
GET https://www.example.com/image.jpeg HTTP/1.1 Accept-Encoding: gzip
Заголовок Accept-Encoding
указывает на то, что серверу разрешается возвращать ресурс с gzip
версией, если он способен делать это. Если сервер принимает этот заголовок во внимание при решении, какой ответ отправить нам, он перечислит его в заголовке Vary
, добавленном к его ответу:
GET https://www.example.com/image.jpeg HTTP/1.1 Status: 200 Cache-Control: max-age: 3600 Content-Encoding: gzip Vary: Accept-Encoding
Конечным результатом этого должно быть то, что кеш должен использовать не только значение URL для кеширования ответа, но и что он должен также использовать значение заголовка запроса Accept-Encoding
для дальнейшей квалификации ключа кеша. Таким образом, если бы другой запрос был сделан с другим значением для Accept-Encoding
заголовка, например deflate
, его ответ не заменил бы указанный запрос gzip
.
Заключение
Кэширование - это удивительно мощный способ повысить производительность вашего приложения, и я рекомендую углубиться в полное руководство MDN по управлению кэшем, если вы хотите изучить его многочисленные закоулки. Я надеюсь, что этот пост дал вам повод для беспокойства по поводу HTTP-кэширования, и если вы заметили ошибку в этом посте, будь то фактическая или синтаксическая, дайте мне знать в комментариях ниже!
Перевод статьи: HTTP Caching