Вникаем в принцип работы HTTP-заголовка

История появления Vary берет свое начало с прекрасной идеи о том, как должен работать веб. В принципе, URL – это не веб-страница, а своего рода ресурс наподобие выписки из банка. Представьте, что вы идете в bank.com и отправляете запрос GET для /statement. Все вроде бы в порядке, но вы не указали, в каком формате нужна выписка. Поэтому браузер отобразит Accept: text/html в ответ на ваш запрос. В теории это означает, что можно указать Accept: text/csv и получить данные в другом формате.

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

Vary: Accept

Это значит, что ответ зависит от значения заголовка Accept в вашем запросе.

Но данный механизм почти не работает в Сети. В теории «согласование контента» было отличной задумкой, но ничего не вышло. Тогда почему этот заголовок все еще популярен и как его расценивают браузеры? Давайте разберемся.

Актуальные случаи использования Vary в браузере

Традиционное использование Vary заключается в согласовании контента с использованием заголовков Accept, Accept-Language и Accept-Encoding. Так сложилось исторически, что первые два с треском провалились. Изменение Accept-Encoding для доставки ответов сжатых, с помощью Gzip или Brotli, в тех случаях, когда они поддерживаются, работает хорошо. В наши дни Gzip поддерживают все браузеры.

Но как реализовать следующие сценарии?

  • Как предоставить изображения, которые соответствуют ширине экрана клиентского устройства? Если пользователь изменит размер окна браузера, будем подгружать новые изображения (в зависимости от Client Hints).
  • Если пользователь выходит из системы, нужно избежать использования любых страниц, которые были кэшированы во время входа в систему (используя cookie как Key).
  • Браузеры, которые поддерживают формат изображений WebP, должны получить именно WebP. Иначе изображения будут предоставляться в формате JPEG.

Кэш от и до

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

Кэш от и до

Некоторые из типов кэша совсем новые. Поэтому понимание того, какой контент кэшируется – это сложный расчет. Вот, что делают эти кэши:

  • Кэш картинок

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

  • Кэш предварительной загрузки

Здесь хранится все, что было предварительно загружено в заголовке Link или в теге <link rel="preload">. Кэширование предварительной загрузки прекращается, если пользователь уходит со страницы.

  • Кэш Service Worker API

Здесь храниться только то, что вы специально поместили в кэш, используя JavaScript в Service Worker. Записи в кэше не будут обновлены без явного запроса к Service Worker в fetch. Service Worker позволяет кэшировать файлы в фоновом режиме. То есть, даже если пользователь офлайн, Service Worker будет работать.

  • Кэш HTTP

Единственный кэш, который работает с заголовками кэша уровня HTTP (Cache-Control). Он имеет самый широкий охват и доступен для всех сайтов. Например, если два сайта загружают один и тот же ресурс, то они могут использовать один кэш.

  • HTTP/2 push-кэш

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

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

К примеру, HTTP/2 сервер отправляет CSS вместе со страницей. И эта же страница также предварительно загружает таблицу стилей с тегом <link rel="preload">. В результате CSS затронет три кэша в браузере. Сначала стили попадет в H2 push-кэш. Когда браузер загрузит страницу и доберется до тега preload, он перенаправит таблицу стилей из push-кэша через HTTP кэш и сохранит ее в кэше предварительной загрузки.

Кэш от и до - 2

Заголовок Vary в качестве валидатора

А что произойдет, если мы добавим заголовок Vary?

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

К Vary стоит относиться как к валидатору. Браузеры вычисляют ключи кэша, используя URL. Когда это происходит, браузеры проверяют, удовлетворяет ли запрос правилам Vary, которые содержатся в кэшированном ответе. Если нет, то браузер обрабатывает запрос как пропущенный кэш и переходит на следующий уровень кэша или выходит в Сеть. В момент получения нового ответа браузер перезапишет кэшированную версию. Хотя технически это другая вариация.

Как ведет себя Vary

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

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

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

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

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

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

Кэширование Service Worker API

Vary работать в браузере по типу CDN. Из этого следует, что браузер должен одновременно хранить только одну вариацию в HTTP-кэше и содержать различные вариации в кэше API. У Firefox (54) это получается, а Google Chrome работает по принципу валидатора, который присутствует в HTTP-кэше.

Кэширование Service Worker API

HTTP-кэш

Основной HTTP-кэш использует Vary в качестве валидатора во всех браузерах.

HTTP/2 push-кэш

Заголовки Vary замечаются, но ни один браузер на самом деле не учитывает их.

HTTP/2 push-кэш

Ошибка «304 (Not Modified)»

Сервер, который отправляет ошибку 304, должен сгенерировать любое из следующих полей заголовка, которое было бы отправлено в ответе 200 (ОК) на тот же запрос: Cache-Control, Content-Location, Date, ETag, Expires, и Vary.

Почему ошибка 304 возвращает заголовок Vary? Краски сгущаются, когда начинаешь читать о том, что необходимо сделать после получения ошибки 304:

Если для обновления выбран сохраненный ответ, кэш должен […] использовать другие поля заголовка, предоставленные в ошибке «304 (Not Modified)», чтобы заменить все экземпляры соответствующих полей заголовка в сохраненном ответе.

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

Client Hints

Client Hints от Google описывает значения, которые могут регулярно меняться во время просмотра сайта пользователем. А именно:

  • DPR

Device pixel ratio – плотность пикселей экрана (может отличаться, если пользователь использует несколько экранов).

  • Save-Data

Если пользователь включил режим сохранения данных.

  • Viewport-Width

Ширина текущего окна просмотра в пикселях.

  • Width

Ширина ресурса в физических пикселях.

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

Заголовок Key

Client Hints и другие заголовки могут эффективно работать в паре с Key, над которым работал Марк. Рассмотрим несколько примеров:

Key: Viewport-Width;div=50

Это значит, что ответ изменяется в зависимости от значения заголовка запроса Viewport-Width. Но округляется до ближайшего числа, кратного 50 пикселям!

Key: cookie;param=sessionAuth;param=flags

Добавление этого заголовка означает, что мы используем два файла cookie: sessionAuth и flags. Если они не изменились, мы можем повторно использовать этот ответ для будущего запроса.

Основные различия между Key и Vary:

  • Key позволяет вносить изменения в поля заголовков, что позволяет изменять cookie.
  • Отдельные значения могут быть объединены в диапазоны, чтобы увеличить вероятность попадания в кэш.
  • Все вариации с одинаковым URL должны иметь одинаковый ключ.

На момент написания статьи ни один браузер или CDN-сервис не поддерживал Key. Хотя в некоторых CDN-сервисах вы получили бы тот же результат, разделив входящие заголовки на несколько частных заголовков.

Применение Variants

Но у некоторых заголовков более сложные правила для своих значений. Представьте, что пришло два запроса с разными значениями Accept-Language– en-gb и en-us. Несмотря на то, что браузер поддерживает изменение языка, у вас установлен только один «English». Если мы ответим на запрос значением US English, тогда этот ответ попадет в кэш CDN и не сможет быть повторно использован для данного запроса. Потому что значения Accept-Language будут разными, а кэш недостаточно «умен», чтобы это понять.

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

Заключение

Что мы поняли:

  • Большинство браузеров видят Vary как валидатор. Если вам нужно кэшировать несколько отдельных вариаций, найдите способ использовать разные URL.
  • Браузеры игнорируют Vary для ресурсов, отправляемых с помощью HTTP/2.
  • Браузеры используют разные типы кэшей. Необходимо понять, как реализация кэширования влияет на производительность каждого из них.
  • Сейчас Vary не так востребован, но эффективная работа Key в паре с Client Hints начинает это менять.

Идите вперед, навстречу изменениям.