Сайт Романа ПарпалакаЗаметкиТехнологииВеб-разработкаIf-Modified-Since и кеширование

If-Modified-Since и кеширование

7 января 2007 года

За что я люблю 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 страница сохраняется в кеше, при последующих запросах выдается только она. Чтобы обновить страницу в кеше, нужно нажать комбинацию клавиш Ctrl + F5, обычная кнопка «Обновить» (F5) не помогает. Нужно отметить, что документы в кеше IE могут храниться очень долго. В Опере страница загружается из кеша при повторном переходе на нее по ссылкам, но кеш очищается по нажатию кнопки «Обновить» или клавиши F5. Следует быть аккуратным, так как CRTL+F5 в Опере — перезагрузка страниц со всех вкладок, которая может затянуться надолго при их большом числе.

Если запретить кеширование страницы функцией 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, вы можете ознакомиться с дополнительной информацией о них.

Поделиться
Посмотрите в блоге

Комментарии

#1. 8 мая 2007 года, 16:04. C пишет:
Спасибо за статью!
А как сделать, чтобы в Опере картинка, выдаваемая скриптом не перезагружалась, можете помочь?

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();
}
#2. 8 мая 2007 года, 17:41. Роман Парпалак пишет:
Вообще вы занимаетесь извращением, если ваша цель — заменить настоящий URL картинки на какой-то другой. Вы усложняете жизнь себе и серверу, на котором будет крутиться ваш скрипт.

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

Если речь идет об академическом интересе, загляните в мою статью об организации RSS (http://written.ru/articles/technologies/si … ilding/rss). Там такая работа с заголовками действительно необходима, поскольку позволяет экономить трафик и на сервере, и пользователям. В статье приведен готовый пример, который у меня корректно работает и с RSS-читалкой Оперы, и с другими читалками, и с ботами Яндекса и Гугла. Замените цикл, выдающий содержимое, на отдачу картинки и уберите сжатие данных (графику сжимать нет смысла, она и так сжата).
#3. 8 мая 2007 года, 21:06. C пишет:
Спасибо за быстрый ответ :)
Кстати, я тоже уж решил сохранять на диск картинку, но у меня mod_rewrite написан так:

RewriteEngine On
RewriteRule ^([^.]+)$ index.php

для создания красивых url и я парсю их в скрипте руками. А как написать, чтобы при любых url открывался скрипт index.php как щас, но с исключением, что если в первом уровне есть show_image, то тогда, чтобы в папку шло, где картинки лежат.
#4. 8 мая 2007 года, 22:48. Роман Парпалак пишет:
В mod_rewrite подробно я не разбирался, поэтому подсказать не смогу. Я бы на вашем месте сделал перенаправления через mod_rewrite на разные скрипты в зависимости от пути (хоть это и не самое лучшее решение).
#5. 1 августа 2008 года, 13:09. Gamer пишет:
Хорошая статья, а как быть со счетчиками? Я хочу учитывать пользователей даже если отдаю им только заголовок, что страница не изменилась. Если бы это был мой собственный счетчик, то все ок, а если я пользуюсь Li.ru ?
#6. 1 августа 2008 года, 14:18. пишет:
Ничего со счетчиками сделать нельзя. Выбирайте, что вам важнее: счетчик liveinternet.ru или экономия трафика как у посетителей сайта, так и у вас на хостинге.
#7. 15 августа 2008 года, 01:20. пишет:
Gamer пишет:

Я хочу учитывать пользователей даже если отдаю им только заголовок, что страница не изменилась. Если бы это был мой собственный счетчик, то все ок, а если я пользуюсь Li.ru?
Можно попробовать после выдачи кода 304 обратиться к серверу liveinternet.ru и эмулировать хит пользователя. Правда, не знаю, как всё это будет работать. Надо бы как-нибудь такое сделать.
#8. 21 августа 2008 года, 14:22. Евгений Че пишет:
Это достаточно интересно. А если ситуация такова, что при обращении к странице через Аякс отдаются параметры ПХП скрипту, он делает картинук и возвращает в ответе что-то типа „<img src=“qwe.jpg»>". В такой ситуации даже если кеш отключен, картинка кешируется браузером и никак не хочет отновляться. Что делать тогда?
#9. 21 августа 2008 года, 17:45. пишет:
Из вашего описания мне не понятна суть проблемы. Опишите подробнее, что вы хотите получить, что вы делаете и что в результате получается.

Если в Ajax серверный скрипт возвращает HTML-код, в котором есть тег img, и вы не хотите, чтобы соответствующая картинка кешировалась, можно к src добавить несущественный параметр вроде src="qwe.jpg?8472", генерируемый случайно.

Если картинка генерируется на лету, и каждый раз меняется, то в скрипте, который ее генерирует, нужно прописать заголовки, запрещающие кеширование.
#10. 21 августа 2008 года, 18:09. пишет:
Так, и еще раз (думаю, последний) по поводу счетчика liveinternet.ru. Предыдущие ответы я писал, не подумав. На самом деле если посмотреть на код счетчика, в параметрах можно заметить Math.random(). Поскольку при открытии страницы каждый раз срабатывает Javascript, адрес меняется и картинка счетчика заново загружается с сервера liveinternet'а. При этом не важно, грузится ли страница заново или берется из кеша.

На всякий случай я только что проверил, что при выдаче ответа 304 Opera, FF и IE загружают счетчик.
#11. 3 марта 2009 года, 16:43. MOVe пишет:
А как в таком случае заставить выполниться JavaScript на кэшированной странице? Ставлю обработчик onload или просто прописываю: < script > код... < /script> и при нажатии стрелок «вперёд», «назад» — JavaScript не выполняется.
#12. 3 марта 2009 года, 17:41. пишет:
Странно. На страницах этого сайта используется как описанное в статье кеширование, так и JavaScript во всплывающих подсказках (http://htmlcoder.visions.ru/JavaScript/?11). Всё работает. Посмотрите, как в скрипте по ссылке отслеживается событие onload.
#13. 4 марта 2009 года, 01:49. MOVe пишет:
Интересная закономерность.

Открываю страницу, которая отдаёт сначала заголовок: 'Last-Modified: ...' и содержимое, потом каждый раз: 'HTTP/1.1 304 Not Modified'.

Если кликать по ссылкам на странице, например: «туда» — «сюда», то событие onload отрабатывает нормально. Но стоит начать нажимать в управлении браузером: «Назад», «Вперёд» (кнопки рядом с адресной строкой), туда-сюда (вперёд-назад) кликать, то javascript не выполняется... Страница остаётся кэшированной и вместе с результатом работы javascript'а в событии onload...

Но стоит покликать в управлении «вперёд» 3 или больше раз, то javascript начинает срабатывать... Как победить вот тот момент, когда пользователь жмёт вперёд-назад, вперёд-назад?
#14. 4 марта 2009 года, 10:38. пишет:
А о каком браузере идет речь? Может, браузер сохраняет для трех страниц не только html, но и javascript в актуальном состоянии (например, значения переменных), так что событие onload не наступает?
#15. 4 марта 2009 года, 12:14. пишет:
Тестирую всё в FireFox 3.0.6, так как могу посмотреть и заголовки, и активность сети в firebug. В нём именно такое поведение.
#16. 4 марта 2009 года, 12:19. MOVe пишет:
IE 6 — корректно отрабатывает. Только что проверил. Надеюсь, не много комментариев :) Хочется добраться до истины.
#17. 4 марта 2009 года, 16:08. пишет:
Сейчас у меня нет времени разбираться с этим вопросом. Может быть потом посмотрю.

Возможно, есть другие решения той задачи, которая перед Вами стоит?
#18. 5 марта 2009 года, 00:17. MOVe пишет:
Решение только одно: оставить как есть, положившись на правильную работу с кэшем двух браузеров: IE и Chrome, а на FireFox, Opera и Safari забить.

Другое решение — отказаться от такого вида кэширования, но тогда придётся каждый раз отдавать клиенту страницу. Либо писать на сервере проверку, если клиент пришёл на предыдущую страницу, то не отдавать ему заголовок: 'HTTP/1.1 304 Not Modified', что само по себе неправильно...

По большому счёту, это уже сам браузер виноват, что он не выполняет скрипты на кэшированной странице. Хотя, может быть всё-таки есть выход. Хотелось бы надеяться. Но ни defer не помогает, ни его отсутствие, ни DOMContentLoaded, ни onload. Возможно, что в этих браузерах нет механизма, запускающего обработку JavaScript'а на странице, которая не была изменена. Хорошо хотя бы, что они правильно обрабатывают переходы по ссылкам — уже плюс. А может и эту ситуацию потом пересмотрят и сделают поддержку в остальных браузерах.

Спасибо за статью, кстати! Буду заглядывать, вдруг найдётся решение :)
#19. 2 июня 2009 года, 14:04. Andrey пишет:
function date2unixstamp($s)
{
return strtotime(substr($s, 5));
}
#20. 2 июня 2009 года, 15:30. пишет:
Точно.
#21. 18 июня 2009 года, 23:19. Саша пишет:
Спасибо большое :) Очень пригодился ваш код.
#22. 28 января 2013 года, 04:32. Макс пишет:
Я конечно не слишком разбираюсь в этом всем. Меня интересует как сделать это на вордпрессе. Как-нибудь можно? Что такое кеширование я слабо представляю, мне нужно, чтобы при посещении сайта роботами на запрос if modified since wordpress выдавал настоящую дату изменения страницы. Ну еще желательно, чтобы при каждом запросе выдавал команду last modified. Это можно на wp сделать?
#23. 28 января 2013 года, 11:02. пишет:
Не знаю, как лучше всего подружить Вордпресс и If-Modified-Since/If-None-Match. Посмотрите, что об этом пишут другие люди, поэкспериментируйте с плагинами.
#24. 28 января 2013 года, 15:02. пишет:
Так то то ж и оно, что не нашел я плагина такого. Вообще, чем больше юзаю движки, тем больше понимаю, что пых надо учить и писать что-то свое.
#25. 22 декабря 2013 года, 22:04. Дмитрий пишет:
Да уж, автор этой статьи видно писал её через копи+пасте.
Для часто обновляемых страниц (я не говорю «динамических», так как страница может каждый раз собираться интерпретатором 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');
}
Это извращение! header('Pragma: no-cache'); — В курсе, что за заголовок? Это старый заголовок протокола http/1.0. Первоисточники нужно читать!!! Остальное описывать не буду, смысла учить Вас нет.
#26. 25 декабря 2013 года, 13:25. пишет:
Вреда от заголовка «Pragma» не будет. Для «пуленепробиваемости» его можно оставить.
#27. 13 мая 2015 года, 17:54. Илья пишет:
по мне так не очень красивый и не очень лаконичный код, для такой простой задачие

особенно умилительно
die;

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

Ваше имя:

Комментарий:

Для выделения используйте следующий код: [i]курсив[/i], [b]жирный[/b].
Цитату оформляйте так: [q = имя автора]цитата[/q] или [q]еще цитата[/q].
Ссылку начните с http://. Других команд или HTML-тегов здесь нет.

Сколько будет 53+2?