Кэш играет важную роль в работе практически любого веб-приложения на уровне работы с базами данных, веб-серверами, а также на клиенте.
В рамках этой статьи мы попытаемся разобраться с клиентским кэшированием. В частности, разберемся с тем, какие http-заголовки используются браузерами и веб-серверами и что они значат.
Но для начала давайте выясним, зачем вообще нужно кэширование на стороне клиента?.
Веб-страницы состоят из множества различных элементов: картинок, css и js файлов и т.п. Часть этих элементов используются на нескольких (многих) страницах сайта. Под клиентским кэшированием понимают способность браузеров сохранять копии файлов (ответов сервера), чтобы не загружать их повторно. Это позволяет значительно ускорить повторную загрузку страниц, сэкономить на трафике, а также снизить нагрузку на сервер.
Существует несколько различных HTTP-заголовков для того, чтобы управлять процессами кэширования на стороне клииента. Давайте поговорим о каждом из них.
Http заголовки для управления клиентским кэшированием
Для начала давайте посмотрим, как сервер и браузер взаимодействуют при отсутствии какого-либо кэширования. Для наглядного понимания я попытался представить и визуализировать процесс общения между ними в виде текстового чата. Представьте на несколько минут, что сервер и браузер — это люди, которые переписываются друг с другом :)
Без кэша (при отсутствии кэширующих http-заголовков)
Как мы видим, каждый раз при отображении картинки cat.png браузер будет снова загружать ее с сервера. Думаю, не нужно объяснять, что это медленно и неэффективно.
Заголовок ответа Last-modified
и заголовок запроса if-Modified-Since
.
Идея заключается в том, что сервер добавляет заголовок Last-modified
к файлу (ответу), который он отдает браузеру.
Last-modified: Fri, 1 Dec 2014 01:01:01 GMT
Теперь браузер знает, что файл был создан (или изменен) 1 декабря 2014. В следующий раз, когда браузеру понадобится тот же файл, он отправит запрос с заголовком if-Modified-Since
.
if-Modified-Since: Fri, 1 Dec 2014 01:01:01 GMT
Если файл не изменялся, сервер отправляет браузеру пустой ответ со статусом 304 (Not Modified)
. В этом случае, браузер знает, что файл не обновлялся и может отобразить копию, которую он сохранил в прошлый раз.
Таким образом, используя Last-modified
мы экономим на загрузке большого файла, отделываясь пустым быстрым ответом от сервера.
Заголовок ответа Etag
и заголовок запроса If-None-Match
.
Принцип работы Etag
очень схож с Last-modified
, но, в отличии от него, не привязан ко времени. Время — вещь относительная.
Идея заключается в том, что при создании и каждом изменении сервер помечает файл особой меткой, называемой ETag
, а также добавляет заголовок к файлу (ответу), который он отдает браузеру:
ETag: "686897696a7c876b7e"
Теперь браузер знает, что файл актуальной версии имеет ETag
равный «686897696a7c876b7e». В следующий раз, когда брузеру понадобится тот же файл, он отправит запрос с заголовком If-None-Match: "686897696a7c876b7e"
.
If-None-Match: "686897696a7c876b7e"
Сервер может сравнить метки и, в случае, если файл не изменялся, отправить браузеру пустой ответ со статусом 304 (Not Modified)
. Как и в случае с Last-modified
браузер выяснит, что файл не обновлялся и сможет отобразить копию из кэша.
Подробнее о ETag
можно почитать здесь.
Заголовок Expired
Принцип работы этого заголовка отличается от вышеописанных Etag
и Last-modified
. При помощи Expired
определяется «срок годности» («срок акуальности») файла. Т.е. при первой загрузке сервер дает браузеру знать, что он не планирует изменять файл до наступления даты, указанной в Expired
:
Expired: Fri, 1 Mar 2014 01:01:01 GMT
В следующий раз браузер, зная, что «дата истечения срока годности» еще не наступила, даже не будет пытаться делать запрос к серверу и отобразит файл из кэша.
Такой вид кэша особенно актуален для иллюстраций к статьям, иконкам, фавиконкам, некоторых css и js файлов и тп.
Заголовок Cache-control
с директивой max-age
.
Принцип работы Cache-control: max-age
очень схож с Expired
. Здесь тоже определяется «срок годности» файла, но он задается в секундах и не привязан к конкретному времени, что намного удобнее в большинстве случаев.
Для справки:
- 1 день = 86400 секунд
- 1 неделя = 604800 секунд
- 1 месяц = 2629000 секунд
- 1 год = 31536000 секунд
К примеру:
Cache-Control: max-age=2629000;
У заголовка Cache-control
, кроме max-age
, есть и другие директивы. Давайте коротко рассмотрим наиболее популярные:
public
Дело в том, что кэшировать запросы может не только конечный клиент пользователя (браузер), но и различные промежуточные прокси, CDN-сети и тп. Так вот, директива public
позволяет абсолютно любым прокси-серверам осуществлять кэширование наравне с браузером.
private
Директива говорит о том, что данный файл (ответ сервера) является специфическим для конечного пользователя и не должен кэшироваться различными промежуточными прокси. При этом она разрешает кэширование конечному клиенту (браузеру пользователя). К примеру, это актуально для внутренних страниц профиля пользователя, запросов внутри сессии и т.п.
no-cache
Позволяет указать, что клиент должен делать запрос на сервер каждый раз. Иногда используется с заголовком Etag
, описанным выше.
no-store
Указывает клиенту, что он не должен сохранять копию запроса или частей запроса при любых условиях. Это самый строгий заголовок, отменяющий любые кэши. Он был придуман специально для работы с конфиденциальной информацией.
must-revalidate
Эта директива предписывает браузеру делать обязательный запрос на сервер для ре-валидации контента (например, если вы используете eTag). Дело в том, что http в определенной конфигурации позволяет кэшу хранить контент, который уже устарел. must-revalidate
обязывает браузер при любых условиях делать проверку свежести контента путем запроса к серверу.
proxy-revalidate
Это то же, что и must-revalidate
, но касается только кэширующих прокси серверов.
s-maxage
Практически не отличается от мах-age
, за исключением того, что эта директива учитывается только кэшем резличных прокси, но не самим браузером пользователя. Буква «s-» исходит из слова «shared» (например, CDN). Эта директива предназначена специально для CDN-ов и других посреднических кэшей. Ее указание отменяет значения директивы max-age
и заголовка Expired
. Впрочем, если вы не строите CDN-сети, то s-maxage
вам вряд ли когда-либо понадобится.
Как посмотреть, какие заголовки используются на сайте?
Вы можете посмотреть заголовки http-запросов (request headers) и ответов (response headers) в отладчике Вашего любимого браузера. Вот например, как это выглядит в хроме:
То-же самое можно увидеть в любом уважающем себя браузере или http-сниффере.
Настройка кэшировения в Аpache и Nginx
Мы не будем пересказывать документацию по настройке популярных серверов. Вы всегда можете посмотреть ее здесь для Nginx и здесь для Apache. Ниже мы приведем несколько примеров из жизни для того, чтобы показать, как выглядят файлы конфигурации.
Пример конфигурации Apache для контроля Expires
Выставляем различный «срок годности» для различных типов файлов. Один год для изображений, один месяц для скриптов, стилей, pdf и иконок. Для всего остального — 2 дня.
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/pdf "access plus 1 month"
ExpiresByType text/x-javascript "access plus 1 month"
ExpiresByType image/x-icon "access plus 1 year"
ExpiresDefault "access plus 2 days"
</IfModule>
Пример конфигурации Nginx для контроля Expires
Выставляем различный «срок годности» для различных типов файлов. Одна неделя — для изображений, один день — для стилей и скриптов.
server {
#...
location ~* \.(gif|ico|jpe?g|png)(\?[0-9]+)?$ {
expires 1w;
}
location ~* \.(css|js)$ {
expires 1d;
}
#...
}
Пример конфигурации Apache для Cache-control (max-age и public/private/no-cache)
<ifModule mod_headers.c>
<FilesMatch "\.(gif|ico)$">
Header set Cache-Control "max-age=2592000, public"
</FilesMatch>
<FilesMatch "\.(js)$">
Header set Cache-Control "max-age=88000,
private, must-revalidate"
</FilesMatch>
<FilesMatch "\.(php)$">
Header set Cache-Control "private, no-store, no-cache,
must-revalidate, no-transform, max-age=0"
Header set Pragma "no-cache"
</FilesMatch>
</ifModule>
Пример конфигурации Nginx для Cache-control статических файлов
server {
#...
location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
add_header Cache-Control "max-age=88000, public";
}
#...
}
В заключение
«Кэшировать все то, что можно кэшировать» — хороший девиз для веб-разработчика. Иногда можно потратить всего несколько часов на конфигурацию и при этом значительно улучшить восприятие вашего сайта пользователем, значительно сократить нагрузку на сервер и сэкономить на трафике. Главное — не переусердствовать и настроить все правильно с учетом особенностей Вашего ресурса.
Будем благодарны за рекомендации и поправки в комментариях и не забудьте поделиться статьей с друзьями, если она Вам понравилась ;)
Отличная заметка Дмитрий!
Из того, что можно добавить, Expires работает для HTTP/1.0 agents, а Cache-control: max-age= появилось в HTTP/1.1
А так же, что для Etag мы как правило должны выполнить все теже действия что и при обычном запросе, чтобы посчитать hash, т.е экономия идет трафика но не CPU
1 день в секундах = 88000
Срочно смените свой калькулятор!
Спасибо, поменяли )) Исправил
Смените ещё раз. 86400 секунд в дне. Вы бы хоть пересчитали)
Мой калькулятор упорно считает в дне 60 * 60 * 24 = 86400
Спасибо, Дима. Всё толково рассказал. Теперь понятно!
Вадим, от тебя особенно приятно слышать!
Спасибо, Дмитрий, за статью.
До ее прочтения не задумывался о клиентском кэшировании.
Теперь есть над чем подумать.
Очень понравилось наглядное представление того, как общаются клиент с сервером.
А что лучше использовать Expires или Cache-control, имеет ли смысл одновременно прописывать и то и другое? Для html-страничек у меня Last-modified, так как это ещё и для поисковых роботов правильно
Теперь ждём статью про Application Cache @ HTML5.
ok :)
Очень хорошо написана статья, удобно разбираться, и тем более очень понятно благодаря прекрасным картинкам))
Вот только заголовок Expires, а не Expired.
Спасибо, очень понятно написано! Очень доступно!
Единственная статья в рунете, где раскрыта вся правда о Кэше и Клиенте!
У меня домашний сервер уже 5 лет, нахожусь в Хабаровске. Кэш на стороне клиента пока не отладил, пока мучаюсь.
Кто хочет попробовать скорость загрузки с личного сервера, заходи на ЕдаМоре.рф.
Дмитрию респект и уважение!
Petr de Cril’on
Спасибо за подробную статью. Хотя, прочитав ее до конца, я так и не усвоил — где мне применить и какую конфигурацию именно. То ли я должен внедрить код в htacsess, то ли я должен хостера попросить об применении кэширования на сервере, то ли я сам могу сделать подобные настройки… Не понял. Прочту еще раз, может где что упустил… Мне бы на практике применить вышесказанное..
Наиболее понятной статьи по кэшированию я не встречал у нас в рунете. Автору низкий поклон и огромное спасибо!
Спасибо. Очень просто и доступно изложено. Особенно порадовали картинки.
Вопрос.
Раньше у меня сайт был на Апаче, и я просто прописывал всё, что нужно в .htaccess. А теперь переехал на Nginх и никак не разберусь, как настроить кеширование. Дело в том, что хостинг виртуальный, доступа к серверу, естественно, нет.
Есть ли возможность настроить кеширование средствами сайта? (php, WP)
Здравствуйте! Я прописал это в конфиге nginx:
location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
add_header Cache-Control "max-age=88000, public";
}
Но в результате на всех css, js и т.д 404 ошибка. В общем не подгружает эти файлы. В чем может быть проблема?
Скажите, могу ли я использовать Cache-Control и одновременно с ним ETag, для того чтобы в случае изменения файлы были перекэшированы, даже если не достигнут max-age?…
если нет, то как можно добиться перекэширования при изменениях?
Классная статья, очень понятно все рассказано, спасибо!
Спасибо за хорошую статью!
По-моему, есть опечатка с годами при описании заголовка Expired — то 2014-й упоминается, то 2015-й.
Как это использовать? Непонятно. Вставляю в .htacces
ExpiresActive On
ExpiresByType image/jpg «access plus 1 year»
ExpiresByType image/jpeg «access plus 1 year»
ExpiresByType image/gif «access plus 1 year»
ExpiresByType image/png «access plus 1 year»
ExpiresByType text/css «access plus 1 month»
ExpiresByType application/pdf «access plus 1 month»
ExpiresByType text/x-javascript «access plus 1 month»
ExpiresByType image/x-icon «access plus 1 year»
ExpiresDefault «access plus 2 days»
совершенно ничего не дает
объяснено получше чем в гугле https://developers.google.com/speed/docs/insights/LeverageBrowserCaching
но по прежнему не понятно
куда вставлять эти куски кода (указаны в примере)?
есть ли DEMO на GitHub?
А что означает last в таком случае? Подскажите, пожалуйста. По этой ссылке не совсем понятно:
https://preply.com/question/chto-oznachaet-last
I see your page needs some fresh & unique articles. Writing manually is time consuming, but there is solution for this.
Just search for; Masquro’s strategies
Как быть с косяком при авторзиации? Ласт модиф потом лезет боком. Надо отключать кеш для старниц поиска и авторизации правильно я понял? Делать
header(«Cache-Control: no-store, no-cache, must-revalidate, max-age=0»);
header(«Cache-Control: post-check=0, pre-check=0», false);
header(«Pragma: no-cache»);
Спасибо за статью, очень наглядно.
Позанудствую, с вашего позволения. Может, кто-то прочитает и задумается.
Про тягу к краткости конфигов неоднократно упоминал автор nginx Игорь Сысоев :)
Регулярка выполняется на каждую картинку — не самое оптимальное распределение ресурсов.
Если переделать в отдельные локейшены под каждый шаблон — серверу не придётся задумываться лишний раз при выдаче каждой конкретной картинки.
location \.png { access_log off; log_not_found off; expires max; gzip off; }
location \.jpg { access_log off; log_not_found off; expires max; gzip off; }
Опечатки: акуальности, кэшировения