If-Modified-Since и кеширование
За что я люблю PHP, так это за то, что гениальные вещи на нем пишутся в несколько строчек. В этой заметке я продолжу рассуждать о правильном использовании заголовков в PHP. Если вам не всё равно, как индексируется поисковиками ваш сайт, если вы хотите сэкономить трафик, вы нашли именно то, что нужно.
Как известно, кеширование на стороне браузера сокращает нагрузку на сервер. Но для часто обновляемых страниц у него есть существенный недостаток: информация в кеше может устареть и не соответствовать действительной информации.
Для каждого документа, отдаваемого сервером, желательно выдавать заголовок Last-Modified (в том числе для правильной индексации, например, Яндексом):
$mt = filemtime($file_name);
header('Last-Modified: '.gmdate('D, d M Y H:i:s', $mt).' GMT');
Для часто обновляемых страниц (я не говорю «динамических», так как страница может каждый раз собираться интерпретатором PHP, но фактически изменяться крайне редко) можно запретить кеширование следующим набором заголовков:
function no_cache()
{
header('Expires: Mon, 26 Jul 1997 00:00:00 GMT');
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
}
В принципе, для удовлетворительной работы сайта этого достаточно. Однако вместо полного запрета кеширования лучше применить более гибкий механизм с использованием заголовка If-Modified-Since. Он присутствует в запросе браузера, если в его кеше есть копия документа, и его значение — некая дата изменения этой копии. PHP-скрипт может посмотреть на эту дату и решить, стоит ли отдавать браузеру свежую страницу, или сообщить, что страница не изменилась, отправив ответ 304 Not Modified. Вместе с отправкой заголовка Last-Modified, код примет вид:
$mt = filemtime($file_name);
$mt_str = gmdate('D, d M Y H:i:s', $mt).' GMT';
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $mt)
{
header('HTTP/1.1 304 Not Modified');
die;
}
header('Last-Modified: '.$mt_str);
echo $text;
В операторе if мы не использовали проверку на равенство $_SERVER['HTTP_IF_MODIFIED_SINCE'] == $mt_str, а преобразовали дату вида Sun, 28 Jan 2007 07:56:48 GMT в формат unixstamp и сравнивали с датой изменения оригинального документа. Это нужно для решения двух проблем.
Дело в том, что последние версии Opera и Firefox исправно копируют содержимое заголовка Last-Modified ответа сервера в заголовок запроса If-Modified-Since (именно поэтому нам нужно было установить Last-Modified), и проверкой на равенство вполне можно было бы обойтись. Но, как всегда, не обошлось без капризов IE 6. Он к заголовку If-Modified-Since добавляет параметр length, в чем и заключается первая проблема. Ее можно решить применением функции strpos, если бы не вторая проблема — хитрости поисковых роботов. Все они (кроме робота Рамблера, который действует по описанной выше схеме) в заголовке If-Modified-Since (если вообще его используют) передают не значение из Last-Modified, а дату последнего скачивания документа. В такой ситуации уже нельзя обойтись без упомянутого перевода дат в unixstamp (что и делает функция date2unixstamp).
Как же работает кеширование в браузерах? Если оно не запрещено вызовом функции no_cache, то в Firefox и в IE страница сохраняется в кеше, при последующих запросах выдается только она. Чтобы обновить страницу в кеше, нужно нажать комбинацию клавиш
Если запретить кеширование страницы функцией no_cache, то Опера и Firefox при обращении к такой странице используют механизм с заголовком If-Modified-Since, и это правильно. То есть кеширование всё равно происходит, но браузер спрашивает у сервера, изменилась ли страница на самом деле, или нет. Однако IE запрет на кеширование воспринимает буквально. В ходе экспериментов стало ясно, что если из трех заголовков no_cache убрать второй, то IE версий 6 и 7 начинает работать так, как нам нужно. Может оказаться полезным корректное использование заголовка Expires. В нем можно установить время, в течение которого будет использоваться только локальная копия документа в кеше. Этот способ позволяет справиться с излишне навязчивым кешированием в IE. Например, чтобы копия в кеше была действительна в течение суток, нужно использовать такой оператор:
header('Expires: '.gmdate('D, d M Y H:i:s', time() + 86400).' GMT');
Итак, как же использовать все эти возможности протокола HTTP? Обработка заголовка If-Modified-Since полезна в любом случае. Например, Яндекс рекомендует ее использовать. Если вы экономите трафик и если страницы обновляются редко, то запрещать их кеширование не нужно. Можно запретить их кеширование, тогда вместо него произойдет запрос к серверу с If-Modified-Since и 304 ответом. Это немного увеличит трафик, но позволит получать более правильную статистику посещений: пользователь зашел на страницу, а мы ему говорим, что страница не изменилась, но в статистике его учитываем. Если документы обновляются часто, практически всегда стоит запрещать их кеширование. Выдача 304 ответа в большинстве случаев скомпенсирует возможное повышение трафика.
Помимо описанного метода для проверки актуальности копии документа в кеше существует еще один, основанный не на дате изменения страницы, а на уникальном хеш-коде содержимого страницы. Общее название для обоих методов — Conditional Get, вы можете ознакомиться с дополнительной информацией о них.
Комментарии
А как сделать, чтобы в Опере картинка, выдаваемая скриптом не перезагружалась, можете помочь?
php.ru/forum/viewtopic.php?p=47843#47843
Вот моя функция:
function show_just_image_v1()
{
global $uri_parser;
$ID=(int)$uri_parser->get_level(1);
$size=(int)$uri_parser->get_level(2);
if($size>0) $ID=get_sized_image_ID_v1($ID, $size);
$path=IMAGE_DIR.$ID;
if(!file_exists($path)) die(«Картинки {$ID} не найдено на сервере.»);
$Tag = md5($ID); //
if(isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
if($_SERVER['HTTP_IF_NONE_MATCH'] == $Tag) {
header('HTTP/1.0 304 Not Modified' );
exit();
}
}
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$browser_date=$_SERVER['HTTP_IF_MODIFIED_SINCE'];
if(str_date_to_int_v1($browser_date)>filemtime($path)) {
header(«HTTP/1.1 304 Not Modified»);
exit();
}
}
$last_modified=gmdate(«D, d M Y H:i:s», filemtime($path)) . « GMT»;
$expires=gmdate(«D, d M Y H:i:s», gmmktime( 1, 1, 1, 1, 1, 2020)) . « GMT»;
header(«Cache-control: private»);
header(«Cache-control: max-age=999999999999»);
header("Expires: " . $expires);
header("Last-Modified: " . $last_modified);
header("ETag: «.$Tag);
header(„Content-Type: image/jpeg“);
$img=imagecreatefromstring(file_get_contents($path));
imagejpeg($img);
exit();
}
Подобные действия могут иметь смысл, если вы каждый раз генерите разные картинки. Но даже и в этой ситуации лучше сохранить картинку на диск, и прописывать в HTML ссылку на нее (если картинка будет актуальна, скажем, для 100 показов страниц).
Если речь идет об академическом интересе, загляните в мою статью об организации RSS (
Кстати, я тоже уж решил сохранять на диск картинку, но у меня mod_rewrite написан так:
RewriteEngine On
RewriteRule ^([^.]+)$ index.php
для создания красивых url и я парсю их в скрипте руками. А как написать, чтобы при любых url открывался скрипт index.php как щас, но с исключением, что если в первом уровне есть show_image, то тогда, чтобы в папку шло, где картинки лежат.
Если в Ajax серверный скрипт возвращает HTML-код, в котором есть тег img, и вы не хотите, чтобы соответствующая картинка кешировалась, можно к src добавить несущественный параметр вроде src="qwe.jpg?8472", генерируемый случайно.
Если картинка генерируется на лету, и каждый раз меняется, то в скрипте, который ее генерирует, нужно прописать заголовки, запрещающие кеширование.
На всякий случай я только что проверил, что при выдаче ответа 304 Opera, FF и IE загружают счетчик.
Открываю страницу, которая отдаёт сначала заголовок: 'Last-Modified: ...' и содержимое, потом каждый раз: 'HTTP/1.1 304 Not Modified'.
Если кликать по ссылкам на странице, например: «туда» — «сюда», то событие onload отрабатывает нормально. Но стоит начать нажимать в управлении браузером: «Назад», «Вперёд» (кнопки рядом с адресной строкой), туда-сюда (вперёд-назад) кликать, то javascript не выполняется... Страница остаётся кэшированной и вместе с результатом работы javascript'а в событии onload...
Но стоит покликать в управлении «вперёд» 3 или больше раз, то javascript начинает срабатывать... Как победить вот тот момент, когда пользователь жмёт вперёд-назад, вперёд-назад?
Возможно, есть другие решения той задачи, которая перед Вами стоит?
Другое решение — отказаться от такого вида кэширования, но тогда придётся каждый раз отдавать клиенту страницу. Либо писать на сервере проверку, если клиент пришёл на предыдущую страницу, то не отдавать ему заголовок: 'HTTP/1.1 304 Not Modified', что само по себе неправильно...
По большому счёту, это уже сам браузер виноват, что он не выполняет скрипты на кэшированной странице. Хотя, может быть всё-таки есть выход. Хотелось бы надеяться. Но ни defer не помогает, ни его отсутствие, ни DOMContentLoaded, ни onload. Возможно, что в этих браузерах нет механизма, запускающего обработку JavaScript'а на странице, которая не была изменена. Хорошо хотя бы, что они правильно обрабатывают переходы по ссылкам — уже плюс. А может и эту ситуацию потом пересмотрят и сделают поддержку в остальных браузерах.
Спасибо за статью, кстати! Буду заглядывать, вдруг найдётся решение :)
{
return strtotime(substr($s, 5));
}
особенно умилительно
die;
— session_cache_limiter
— session_cache_expire
//
// set the cache limiter to:
//
// public
// ------
// Expires: (когда-нибудь в будущем, в зависимости от session.cache_expire)
// Cache-Control: public, max-age=(когда-нибудь в будущем, в зависимости от session.cache_expire)
// Last-Modified: (временная метка последнего сохранения сессии)
//
// private_no_expire
// -----------------
// Cache-Control: private, max-age=(session.cache_expire в будущем), pre-check=(session.cache_expire в будущем)
// Last-Modified: (временная метка последнего сохранения сессии)
//
// private
// -------
// Expires: Thu, 19 Nov 1981 08:52:00 GMT
// Cache-Control: private, max-age=(session.cache_expire в будущем), pre-check=(session.cache_expire в будущем)
// Last-Modified: (временная метка последнего сохранения сессии)
//
// nocache
// -------
// Expires: Thu, 19 Nov 1981 08:52:00 GMT
// Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
// Pragma: no-cache
Оставьте свой комментарий