Вникаем в принцип работы 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">. Кэширование предварительной загрузки прекращается, если пользователь уходит со страницы.
Здесь храниться только то, что вы специально поместили в кэш, используя 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 кэш и сохранит ее в кэше предварительной загрузки.

Заголовок 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-кэше.

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

Ошибка «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 начинает это менять.
Идите вперед, навстречу изменениям.