Кэширование веб-ресурсов: на стороне клиента

Кэширование веб-ресурсов: на стороне клиента

1 декабря 2022 г.

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

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

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

Проблема в том, что «вычисление» не является математическим. В математике результат вычисления постоянен во времени. В Интернете ресурс, который вы запросили вчера, может быть другим, если вы запросите его сегодня. Например, подумайте о прогнозе погоды. Все сводится к двум взаимосвязанным понятиям: свежесть и несвежесть.

<цитата>

Свежий ответ – это ответ, возраст которого еще не истек. И наоборот, устаревший ответ — это тот, где он есть.

Время жизни ответа — это промежуток времени между его генерацией сервером-источником и временем его истечения. Явное время истечения срока действия — это время, когда исходный сервер предполагает, что сохраненный ответ больше не может использоваться Cache без дополнительной проверки, тогда как эвристическое время истечения назначается Cache< /code>, когда не указано явное время истечения срока действия. Возраст ответа — это время, прошедшее с момента его создания или успешной проверки исходным сервером.

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

-- RFC 7234 - 4.2. Свежесть

Раннее кэширование веб-ресурсов

Помните, что в начале своего существования WWW была относительно простой по сравнению с сегодняшним днем. Клиент отправлял запрос, а сервер возвращал запрошенный ресурс. Когда ресурс был страницей, была ли это статическая страница или страница, отображаемая сервером, не имело значения. Следовательно, раннее кэширование на стороне клиента было довольно «простым».

Первая спецификация веб-кэширования определена в RFC 7234, aka HTTP/1.1 Caching. , в 2014 г. Обратите внимание, что с 2022 г. он был заменен RFC 9111.

Я не буду говорить здесь о HTTP-заголовке Pragma, так как он устарел. Самый простой способ управления кешем — через заголовок ответа Expire. Когда сервер возвращает ресурс, он указывает, после какой временной метки кэш устарел. У браузера есть два варианта запроса кэшированного ресурса:

* Либо текущее время до отметки времени истечения срока действия: ресурс считается свежим, и браузер обслуживает его из локального кеша * Или это после: ресурс считается устаревшим, и браузер требует ресурс с сервера, так как он не кэшировался

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

* Решение об использовании локально кэшированного ресурса (или нет) основано на эвристике. Ресурс мог быть изменен на стороне сервера, несмотря на то, что значение Expiry находится в будущем, поэтому браузер обслуживает устаревший ресурс. И наоборот, браузер может отправить запрос, потому что время истекло, а ресурс не изменился. * Более того, Expire довольно прост. Ресурс либо свежий, либо устаревший; либо верните его из Кэша, либо отправьте запрос еще раз. Мы можем захотеть иметь больше контроля.

Cache-Control спешит на помощь

Заголовок Cache-Control отвечает следующим требованиям:

* Никогда не кэшируйте ресурс вообще * Проверить, должен ли ресурс обслуживаться из кеша, прежде чем обслуживать его * Могут ли промежуточные кеши (прокси) кэшировать ресурс?

Cache-Control – это HTTP-заголовок, используемый в запросе и ответе. Заголовок может содержать различные директивы, разделенные запятыми. Точные директивы различаются в зависимости от того, являются ли они частью запроса или ответа.

В целом, Cache-Control довольно сложен. Это могло бы стать предметом отдельного поста; Я не буду перефразировать спецификацию.

Однако вот наглядная справка о том, как настроить заголовки ответов Cache-Control.

На странице Cache-Control Mozilla Developer Network есть некоторые важные варианты использования Cache-Control вместе с настройкой.

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

Последнее изменение и ETag

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

<цитата>

Заголовок HTTP запроса If-Modified-Since делает запрос условным: сервер возвращает запрошенный ресурс со статусом 200, только если он был последним изменены после указанной даты. Если с тех пор ресурс не изменялся, ответ представляет собой 304 без какого-либо тела; заголовок ответа Last-Modified предыдущего запроса содержит дату последней модификации. В отличие от If-Unmodified-Since, If-Modified-Since можно использовать только с GET или HEAD.

-- If-Modified-Since

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

Примечание: If-Unmodified-Since имеет противоположную функцию для POST и других неидемпотентных методов. Он возвращает HTTP-ошибку 412 Precondition Failed, чтобы избежать перезаписи измененных ресурсов.

Проблема с отметками времени в распределенных системах заключается в том, что невозможно гарантировать, что все часы в системе имеют одинаковое время. Часы двигаются с разной скоростью, и их необходимо синхронизировать с одним и тем же временем через равные промежутки времени. Следовательно, если сервер, сгенерировавший заголовок Last-Modified, и сервер, получивший заголовок If-Modified-Since, различаются, результаты могут быть неожиданными в зависимости от их дрейфа. . Обратите внимание, что это также относится к заголовку Expire.

Etags — это альтернатива временным меткам, позволяющая избежать описанной выше проблемы. Сервер вычисляет хэш обслуживаемого ресурса и отправляет заголовок ETag, содержащий значение, вместе с ресурсом. Когда приходит новый запрос с If-None-Match, содержащим хеш-значение, сервер сравнивает его с текущим хэшем. Если они совпадают, возвращается 304, как указано выше.

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

API кеша

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

Вот предоставленные методы:

| Метод | Описание | |----|----| | Cache.match(запрос, параметры) | Возвращает Promise, который преобразуется в ответ, связанный с первым соответствующим запросом в объекте Cache. | | Cache.matchAll(запрос, параметры) | Возвращает Promise, который преобразуется в массив всех совпадающих ответов в объекте Cache. | | Cache.add(запрос) | Принимает URL-адрес, извлекает его и добавляет полученный объект ответа в заданный кеш. | | Функционально это эквивалентно вызову fetch() с последующим использованием put() для добавления результатов в кеш. | | | Cache.addAll(запросы) | Принимает массив URL-адресов, извлекает их и добавляет полученные объекты ответа в заданный кеш. | | Cache.put(запрос, ответ) | Принимает как запрос, так и его ответ и добавляет их в заданный кеш. | | Cache.delete(запрос, параметры) | Находит запись Cache, ключ которой является запросом, возвращая Promise, который разрешается в true, если совпадающая запись Cache найден и удален. Если запись Cache не найдена, Promise разрешается в false. | | Cache.keys(запрос, параметры) | Возвращает Promise, который преобразуется в массив ключей Cache. |

Cache API работает в сочетании с Service Workers. Схема проста:

  1. Вы регистрируете сервисного работника по URL-адресу
  2. Браузер вызывает worker перед вызовом получения URL
  3. Из воркера можно вернуть ресурсы из кеша и избежать любых запросов к серверу

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

Обзор

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

| Заказать | Альтернатива | Управляется | Местный | Плюсы | Минусы | |----|----|----|----|----|----| | 1 | Service Worker + Cache API | Вы | Да | Гибкий | - Требуются навыки кодирования JavaScript n - Время кодирования и обслуживания | | 2 | Срок действия | Браузер | Да | Простая конфигурация | - Основанный на предположении n - Упрощенный | | 2 | Кэш-контроль | Браузер | Да | Детальный контроль | - На основе предположений n - Сложная конфигурация | | 3 | Последнее изменение | Браузер | Нет | Просто работает | Чувствительный к дрейфу часов | | 3 | ETag | Браузер | Нет | Просто работает | Немного более чувствителен к ресурсам для вычисления хэша |

Обратите внимание, что эти альтернативы не являются исключительными. У вас может быть короткий заголовок Expire и полагаться на ETag. Вероятно, вам следует использовать как альтернативу уровня 2, так и альтернативу уровня 3.

Немного практики

Давайте применим теорию, которую мы рассмотрели выше, на практике. Я настрою двухуровневый HTTP-кеш:

  • Первый уровень кэширует ресурсы локально на 10 секунд с помощью Cache-Control
  • .
  • Второй уровень использует ETag, чтобы избежать оптимизации загрузки данных по сети.

Я буду использовать Apache APISIX. APISIX стоит на плече гигантов, а именно NGINX. NGINX добавляет заголовки ответа ETag по умолчанию.

Нам нужно только добавить заголовок ответа Cache-Control. Мы достигаем этого с помощью плагина response-rewrite:

upstreams:
  - id: 1
    type: roundrobin
    nodes:
      "content:8080": 1
routes:
  - uri: /*
    upstream_id: 1
    plugins:
      response-rewrite:
        headers:
          set:
            Cache-Control: "max-age=10"

Сначала сделаем это без браузера.

curl -v localhost:9080
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 147
Connection: keep-alive
Date: Thu, 24 Nov 2022 08:21:36 GMT
Accept-Ranges: bytes
Last-Modified: Wed, 23 Nov 2022 13:58:55 GMT
ETag: "637e271f-93"
Server: APISIX/3.0.0
Cache-Control: max-age=10

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

curl -H 'If-None-Match: "637e271f-93"' -v localhost:9080

Результатом является 304 Not Modified, как и ожидалось:

HTTP/1.1 304 Not Modified
Content-Type: text/html; charset=utf-8
Content-Length: 147
Connection: keep-alive
Date: Thu, 24 Nov 2022 08:26:17 GMT
Accept-Ranges: bytes
Last-Modified: Wed, 23 Nov 2022 13:58:55 GMT
ETag: "637e271f-93"
Server: APISIX/3.0.0
Cache-Control: max-age=10

Теперь мы можем сделать то же самое внутри браузера. Если мы воспользуемся функцией повторно повторно до истечения 10 секунд, браузер вернет ресурс из кеша, не отправляя запрос на сервер.

Заключение

В этом посте я описал несколько альтернатив кешированию веб-ресурсов: Expiry и Cache-Control, Last-Modified и ETag, а также Cache API и веб-воркеры.

Вы можете легко установить заголовки ответа HTTP через обратный прокси-сервер или шлюз API. В Apache APISIX теги ETag включены по умолчанию, а другие заголовки легко настраиваются.

В следующем посте я опишу кеширование на стороне сервера.

Исходный код этого сообщения можно найти на GitHub.

Дальше:

* RFC 7234: HTTP/1.1: кэширование (устарело) * RFC 9111: кэширование HTTP * Кэширование HTTP * Управление кешем * Предотвратите ненужные сетевые запросы с помощью кэша HTTP * API кеша * Кэширование Service Worker и HTTP-кэширование


Первоначально опубликовано на сайте A Java Geek 27 ноября 2022 г.


Оригинал