HTTP-кэширование

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

Эта схема демонстрирует взаимодействие клиентов с сервером через прокси
Взаимодействие клиентов с сервером

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

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

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

Как работает кэширование

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

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

Если закэшированные данные найдены, но уже устарели, то запрос направляется дальше, дополненный специальными заголовками-валидаторами, позволяющими серверу-источнику подтвердить или опровергнуть пригодность кэша. Если тот актуален, то вместо запрашиваемых данных сервер может отправить единственный заголовок (т. н. код 304), разрешающий использовать кэш. Этот механизм называется валидацией, или условным кэшированием.

Таким образом, управление HTTP-кэшированием сайта сводится к отправке заголовков отклика, сообщающих правила его кэширования и срок годности, а также проверке входящих заголовков GET-запросов на предмет свежести найденного кэша.

ПРИМЕЧАНИЕ: независимо от заголовков отклика, не могут быть закэшированы данные, запрошенные по безопасному протоколу HTTPS или методом, отличным от GET.

HTTP-заголовки

Наиболее простой, но наименее эффективный способ управления кэшированием — применение тегов <meta />. Они размещаются внутри блока <head>, в HTML коде, и потому попросту игнорируются прокси-серверами, которые HTML не читают.

Гораздо более эффективны HTTP-заголовки. Они не видны в HTML коде и отправляются перед запрашиваемыми данными. Некоторые заголовки отправляются веб-сервером автоматически, однако вы можете переопределять и их, просто устанавливая необходимые значения. В языке PHP для отправки заголовков предназначена функция header(), единственным параметром которой является собственно сам заголовок. Помните, что отправлять их можно только до начала вывода данных.

Заголовок Expires

Одним из основных в механизме кэширования является заголовок Expires. В нем сообщается срок годности представленных в отклике данных. В значении указывается дата и время по Гринвичу (в формате RFC 1123), по истечении которых кэш должен считаться устаревшим, а запросы перенаправляться на сервер-источник для проверки его актуальности или получения свежего отклика:

Expires: Mon, 18 Oct 2010 14:15:00 GMT

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

Применение заголовка Expires предполагает, что часы в кэше и на сервере-источнике синхронизированы. В противном случае одна и та же метка времени может трактоваться по-разному. Также не следует забывать обновлять срок годности, когда это необходимо. Иначе по его истечению кэширование перестанет работать, и все запросы станут поступать на сервер.

Заголовок Cache-Control

Более гибким инструментом кэширования является заголовок Cache-Control. Он поддерживает целый ряд директив, которые можно указывать через запятую. Например:

Cache-Control: max-age=3600, must-revalidate

Ниже приведены допустимые директивы для заголовка отклика:

ДирективаОписание
max-age=nУстанавливает срок годности ответа равным n секунд после запроса, что, в отличие от Expires, позволяет не беспокоиться о синхронизации времени и обновлении срока годности.
s-maxage=nТо же, что и max-age, но применимо к разделяемым (т. е. прокси) кэшам.
publicРазрешает кэширование запросов, защищенных аутентификацией, которые по умолчанию не кэшируются.
privateРазрешает кэширование только в однопользовательских кэшах (т. е. браузерах), запрещая его в разделяемых. Применяется, например, когда разным пользователям отдаются различные версии страницы. Однако этот метод не гарантирует конфиденциальность данных. Для этого рекомендуется использовать шифрованный протокол HTTPS, который вообще не кэшируется.
no-cacheОбязывает кэш направлять все запросы на сервер-источник для подтверждения свежести хранимого в кэше отклика. Применяется, если необходимо сохранить семантическую прозрачность, не теряя преимуществ условного кэширования.
no-storeЗапрещает кэшировать отклик при любых условиях. Как и private, не является надежным средством защиты информации от третьих лиц.
must-revalidateОбязывает кэш жестко следовать предоставленной информации о свежести хранимого отклика. Обычно при определенных обстоятельствах (например, при отсутствии интернет-соединения) HTTP позволяет использовать просроченный кэш. Указывая этот заголовок, вы сообщаете о строгой необходимости следовать установленным правилам.
proxy-revalidateТо же, что и must-revalidate, но применимо к разделяемым кэшам.
no-transformНекоторые прокси-сервера могут в каких-либо целях преобразовывать заголовки и даже тело отклика (изменять формат изображения, дописывать заголовки, менять их порядок и т. п.). Данная директива запрещает любые подобные действия.

В случае указания противоречивых директив приоритетными являются более прозрачные, т. е. обеспечивающие свежесть предоставляемых клиенту данных. В свою очередь директива max-age приоритетней, чем заголовок Expires.

HTTP/1.0 и заголовок Pragma

Следует упомянуть, что заголовок Cache-Control появился в протоколе HTTP/1.1, а значит клиенты и прокси, поддерживающие только HTTP/1.0, не поймут его директив. Поэтому в целях совместимости Expires можно оставить даже при наличии max-age, а если нужно запретить использование кэша, можно также указать дополнительный заголовок из протокола HTTP/1.0:

Pragma: no-cache

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

Валидация, или условное кэширование

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

Наиболее распространенным валидатором является Last-Modified. Сервер может отправлять его с каждым откликом, сообщая в нем дату и время последнего изменения запрашиваемой страницы или файла. На языке PHP это может выглядеть так:

header('Last-Modified: Mon, 18 Oct 2010 14:15:00 GMT');

Значение может храниться в файловой системе, базе данных или вычисляться каким-либо другим образом — не суть важно. Клиент, закэшировав отклик с таким заголовком, в дальнейшем будет возвращать его значение в заголовке запроса If-Modified-Since, как бы спрашивая, изменялся ли документ с тех пор. Сравнивая полученный If-Modified-Since с реальным значением Last-Modified сервер решает, можно ли использовать кэшированный отклик или следует послать свежий:

//любым подходящим способом получаем метку времени Unixtime
$modified = filemtime($filename);

//процесс валидации
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])){
   $cacheModified = preg_replace('/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE']);
   if ($modified <= strtotime($cacheModified)){
      header('HTTP/1.1 304 Not Modified');
      exit();
   }
}

//ниже формируем свежий отклик

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

Протокол HTTP/1.1 предлагает альтернативный способ определения свежести кэша с помощью валидатора ETag, представляющем собой уникальный идентификатор отклика — произвольную строку, заключенную в кавычки. Этот метод работает аналогичным образом. Сервер должен прикреплять к отклику заголовок ETag. Eсли в кэше найден подходящий отклик, то клиентский запрос снабжается заголовком If-None-Match с той же меткой. Сверив метки на сервере можно сделать соответствующие выводы, и если кэш достаточно свеж, отправить клиенту «304 Not Modified».

$eTag = md5($content);
header('ETag: "'.$eTag.'"');
if (isset($_SERVER['HTTP_IF_NONE_MATCH'])){
   if ($eTag == substr($_SERVER['HTTP_IF_NONE_MATCH'], 1, -1)){
      header('HTTP/1.1 304 Not Modified');
      exit();
   }
}

//ниже формируем свежий отклик

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

Большинство современных веб-серверов автоматически отправляют заголовки ETag и Last-Modified для статических файлов, однако в случае динамических страниц позаботиться об этом нужно самостоятельно. Для большей совместимости можно применять оба валидатора.

Заголовок Connection

Вместе с откликом «304 Not Modified» можно отправлять и другие заголовки, чтобы обновить их значения в кэше. И наоборот, заголовок Connection позволяет перечислить названия тех заголовков, которые обновлять не нужно:

Connection: Expires

Трюк в том, что перечисленные заголовки будут причислены к «заголовкам соединения», которые по умолчанию не кэшируются (это Connection, Keep-Alive, Proxy-Authenticate, Proxy-Authorization, TE, Trailers, Transfer-Encoding и Upgrade).

Заголовок Vary

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

Однако не всегда возможно организовать уникальные URL каждому варианту отображения. Например, при использовании технологии сжатия страниц, кэш должен отдельно хранить сжатую и несжатую версии, поскольку не каждый клиент поддерживает gzip-сжатие.

Для подобных случаев предусмотрен заголовок Vary. Он позволяет сообщить кэшу названия тех заголовков запроса, при изменении которых сервер возвращает другой вариант документа. Например, если отображение страницы зависит от браузера и того, поддерживает ли он gzip-сжатие, то вместе с откликом серверу следует отправлять заголовок:

Vary: Accept-Encoding, User-Agent

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

Кэширование средствами Apache

Если ваш сайт работает под управлением веб-сервера Apache, то в вашем распоряжении могут оказаться два весьма полезных модуля — mod_expires и mod_headers. По умолчанию они входят в конфигурацию сервера, но выключены. Чтобы проверить их наличие, введите в консоли httpd -l (а также httpd -M в последних версиях Apache) или, например, воспользуйтесь PHP-функцией phpinfo(). Если модули недоступны, то для их установки понадобится административный доступ к серверу. Подробности можно узнать в руководстве веб-сервера Apache.

Модуль mod_expires управляет установкой заголовка Expires и директивы max-age заголовка Cache-Control. С его помощью можно определять срок годности кэша относительно времени последнего изменения физического файла на сервере, последнего доступа к нему или абсолютным значением даты/времени.

Модуль mod_headers позволяет управлять любыми заголовками, включая установку прочих директив Cache-Control. Команды обоих модулей прописываются в конфигурации Apache или в файле .htaccess, если такая возможность включена администратором. Например так:

### включить mod_expires
ExpiresActive On
### Срок годности gif-файлов истекает через месяц после доступа к ним
ExpiresByType image/gif A2592000
### Срок годности прочих файлов — 1 день от даты последнего изменения
ExpiresDefault "modification plus 1 day"
### Добавить заголовок Cache-Control для файла index.html
<Files index.html>
Header append Cache-Control "public, must-revalidate"
</Files>

Обратите внимание, что модуль mod_expires добавляет директиву max-age автоматически. Преимуществом использования этих модулей является возможность управлять кэшированием как динамических страниц, так и статических файлов на диске. Однако в гибкости кэширования динамических страниц они несколько уступают управлению из серверных скриптов, поскольку правила их жестко определяются в настройках сервера. Подробнее об этих модулях можно узнать из руководств mod_expires и mod_headers.

10 полезных рекомендаций

  1. Старайтесь указывать срок годности документов и файлов в соответствии с регулярностью их обновления. Статическим данным можно указать большое значение max-age, ежедневно обновляемым страницам — около суток и т. д.

  2. По возможности делайте каждый документ доступным только по одному URL. Не передавайте пользовательские данные через URL, если только генерируемая страница целиком не предназначена одному пользователю.

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

  4. Следите, чтобы заголовок Last-Modified соответствовал реальной дате изменения содержимого. Не пересохраняйте файлы и страницы, если не собираетесь их менять.

  5. Минимизируйте использование SSL, а POST-запросы используйте только там, где это необходимо.

  6. Старайтесь избегать ситуаций, когда отображаемые данные зависят от куки. Их сложно кэшировать. Если на странице есть элементы, отображение которых отличается для разных пользователей, приемлемым решением может оказаться подгрузка их с помощью технологии AJAX.

  7. Сокращайте количество ресурсов, требующих HTTP-аутентификации. Например, изображения на защищенной паролем странице обычно не должны требовать аутентификации. Вынесите их в директорию со свободным доступом. Если вы хотите кэшировать страницы, защищенные паролем, используйте заголовок:

    Cache-Control: public, no-cache
  8. Отправляйте заголовок Content-Length с указанием длины тела отклика в байтах (без длины заголовков). HTTP устроен так, что это позволит клиенту отправлять несколько запросов по одному соединению одновременно.

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

  10. Проверить отправляемые сервером заголовки можно, например, на REDbot или с помощью этого плагина для Firefox.

13 комментариев

Хорошая статья

roman @ 14 мая 2011

Отличная статья.
Давно искал решение проблемы с
Vary: Accept-Encoding

Доступно расписано.Спасибо

viktor37 @ 18 мая 2011

Пожалуйста :)

Алексей @ 18 мая 2011

Отличная статья, сейчас сам занимаюсь серверной оптимизацией сайта. Хочу уменьшить отклик сайта, поставил Lighthttpd но он жрет много озушки и почему то создаются куча php-cgi процессы, решил поставить nginx фронтендом и передавать статику через него и апач бекэндом, хочу сделать что бы отклик сайта был еще меньше. :)))

mindwork @ 23 мая 2011

Mindwork, спасибо за отзыв.

Конечно, использование nginx для статики предпочтительнее. Ускорение отклика будет существенным, если у вас высоконагруженный проект и Apache не успевает отдавать контент.

Ускорить сайт в глазах конечного пользователя можно также с помощью клиентской оптимизации (спрайты, сжатие текста, оптимизация верстки и JS). Описание методов можно почерпнуть в книге «Реактивные веб-сайты. Мациевский, Степанищев, Кондратенко». Там, кстати, есть и раздел о настройке сжатия в nginx и lighttpd.

Алексей @ 23 мая 2011

Для меня очень полезная статья!

Максим @ 12 июля 2011

Спасибо, статья помогла, а то уже устал в сражении с гугловский Page Speed Online

WarGot @ 29 июля 2011

Хорошая статья. Спасибо!

Ярослав @ 4 марта 2012

Отличный материал

Сергей @ 5 октября 2012

Спасибо, очень полезная статья. У меня просто есть такой вопрос, у меня есть слайдер изображений на сайте, и когда я стартую слайдер , он работает так, по старту он начиает все картинки один за другого переходить. И как мне делать чтоб они не сохранялись вообще в кэше. Спасибо заранее

Гурген @ 17 марта 2013

Гурген, запретить кэширование изображений можно средствами веб-сервера, отправляя вместе с ними заголовок Cache-Control с директивой no-store. Для этого разместите в папке с картинками файл .htaccess со следующим содержимым:

<FilesMatch \.(gif|jpg|png)$>
   Header append Cache-Control "no-store"
</FilesMatch>

Алексей @ 25 марта 2013

Отличная статья. Мне только осталось непонятно
1) что будет, если значения Expired и max-age будут отличаться?
2) если мы закешировали (с помощью Expired например) файл на сутки и в течении этих суток изменили этот файл, то добиться того, чтобы юзер скачал актуальную версию можно только паереименованием файла? И даже заголовок Last-Modified тут не подействует?

Дмитрий @ 26 августа 2013

Дмитрий,

1) Директива max-age более приоритетна, независимо от значения заголовка Expires.

2) Если в кэше есть свежая (подходящая по сроку годности) версия файла, то запрос попросту не дойдет до сервера. Если это недопустимо, можно воспользоваться условным кэшированием. Для этого отправляйте вместе с файлом заголовок Cache-Control с директивой no-cache и настройте сервер таким образом, чтобы он возвращал 304-й заголовок, если закэшированный файл все еще актуален.

Алексей @ 28 августа 2013

Оставить комментарий