Умное Кеширование и Версионность в Javascript/CSS
Подключая внешние CSS и Javascript, мы хотим снизить до минимума лишние HTTP-запросы.
Для этого .js и .css файлы отдаются с заголовками, обеспечивающими надежное кеширование.
Но что делать, когда какой-то из этих файлов меняется в процессе разработки? У всех пользователей в кеше старый вариант - пока кеш не устарел, придет масса жалоб на сломанную интеграцию серверной и клиентской части.
Правильный способ кеширования и версионности полностью избавляет от этой проблемы и обеспечивает надежную, прозрачную синхронизацию версий стиля/скрипта.
Самый простой способ кеширования статических ресурсов - использование ETag.
Достаточно включить соответствующую настройку сервера (для Apache включена по умолчанию) - и к каждому файлу в заголовках будет даваться ETag - хеш, который зависит от времени обновления, размера файла и (на inode-based файловых системах) inode.
Браузер кеширует такой файл и при последующих запросах указывет заголовок If-None-Match с ETag кешированного документа. Получив такой заголовок, сервер может ответить кодом 304 - и тогда документ будет взят из кеша.
Выглядит это так:
- Первый запрос к серверу (кеш чистый)
-
GET /misc/pack.js HTTP/1.1
Host: javascript.ru
Вообще, браузер обычно добавляет еще пачку заголовоков типа User-Agent, Accept и т.п. Для краткости они порезаны.
- Ответ сервера
- Сервер посылает в ответ документ c кодом 200 и ETag:
HTTP/1.x 200 OK
Content-Encoding: gzip
Content-Type: text/javascript; charset=utf-8
Etag: "3272221997"
Accept-Ranges: bytes
Content-Length: 23321
Date: Fri, 02 May 2008 17:22:46 GMT
Server: lighttpd
- Следующий запрос браузера
-
При следующем запросе браузер добавляет
If-None-Match: (кешированный ETag):
GET /misc/pack.js HTTP/1.1
Host: javascript.ru
If-None-Match: "453700005"
- Ответ сервера
-
Сервер смотрит - ага, документ не изменился. Значит можно выдать код 304 и не посылать документ заново.
HTTP/1.x 304 Not Modified
Content-Encoding: gzip
Etag: "453700005"
Content-Type: text/javascript; charset=utf-8
Accept-Ranges: bytes
Date: Tue, 15 Apr 2008 10:17:11 GMT
Альтернативный вариант - если документ изменился, тогда сервер просто посылает 200 с новым ETag.
Аналогичным образом работает связка Last-Modified + If-Modified-Since:
- сервер посылает дату последней модификации в заголовке
Last-Modified (вместо ETag)
- браузер кеширует документ, и при следующем запросе того же документа посылает дату закешированной версии в заголовке
If-Modified-Since(вместо If-None-Match)
- сервер сверяет даты, и если документ не изменился - высылает только код 304, без содержимого.
Эти способы работают стабильно и хорошо, но браузеру в любом случае приходится делать по запросу для каждого скрипта или стиля.
Общий подход для версионности - в двух словах:
- Во все скрипты добавляется версия (или дата модификации). Например, http://javascript.ru/my.js превратится в http://javascript.ru/my.v1.2.js
- Все скрипты жестко кешируются браузером
- При обновлении скрипта версия меняется на новую: http://javascript.ru/my.v2.0.js
- Адрес изменился, поэтому браузер запросит и закеширует файл заново
- Старая версия 1.2 постепенно выпадет из кеша
Дальше мы разберем, как сделать этот процесс автоматическим и прозрачным.
Жесткое кеширование - своего рода кувалда которая полностью прибивает запросы к серверу для кешированных документов.
Для этого достаточно добавить заголовки Expires и Cache-Control: max-age.
Например, чтобы закешировать на 365 дней в PHP:
header("Expires: ".gmdate("D, d M Y H:i:s", time()+86400*365)." GMT");
header("Cache-Control: max-age="+86400*365);
Или можно закешировать контент надолго, используя mod_header в Apache:
Header add "Expires" "Mon, 28 Jul 2014 23:30:00 GMT"
Header add "Cache-Control" "max-age=315360000"
Получив такие заголовки, браузер жестко закеширует документ надолго. Все дальнейшие обращения к документу будут напрямую обслуживаться из кеша браузера, без обращения к серверу.
Большинство браузеров (Opera, Internet Explorer 6+, Safari) НЕ кешируют документы, если в адресе есть вопросительный знак, т.к считают их динамическими.
Именно поэтому мы добавляем версию в имя файла. Конечно, с такими адресами приходится использовать решение типа mod_rewrite, мы это рассмотрим дальше в статье.
P.S А вот Firefox кеширует адреса с вопросительными знаками..
Разберем, как автоматически и прозрачно менять версии, не переименовывая при этом сами файлы.
Самое простое - это превратить имя с версией в оригинальное имя файла.
На уровне Apache это можно сделать mod_rewrite:
RewriteEngine on
RewriteRule ^/(.*\.)v[0-9.]+\.(css|js|gif|png|jpg)$ /$1$2 [L]
Такое правило обрабатывает все css/js/gif/png/jpg-файлы, вырезая из имени версию.
Например:
/images/logo.v2.gif -> /images/logo.gif
/css/style.v1.27.css -> /css/style.css
/javascript/script.v6.js -> /javascript/script.js
Но кроме вырезания версии - надо еще добавлять заголовки жесткого кеширования к файлам. Для этого используются директивы mod_header:
Header add "Expires" "Mon, 28 Jul 2014 23:30:00 GMT"
Header add "Cache-Control" "max-age=315360000"
А все вместе реализует вот такой апачевый конфиг:
RewriteEngine on
# убирает версию, и заодно ставит переменную что файл версионный
RewriteRule ^/(.*\.)v[0-9.]+\.(css|js|gif|png|jpg)$ /$1$2 [L,E=VERSIONED_FILE:1]
# жестко кешируем версионные файлы
Header add "Expires" "Mon, 28 Jul 2014 23:30:00 GMT" env=VERSIONED_FILE
Header add "Cache-Control" "max-age=315360000" env=VERSIONED_FILE
Из-за порядка работы модуля mod_rewrite, RewriteRule нужно поставить в основной конфигурационный файл httpd.conf или в подключаемые к нему(include) файлы, но ни в коем случае не в .htaccess, иначе команды Header будут запущены первыми, до того, как установлена переменная VERSIONED_FILE.
Директивы Header могут быть где угодно, даже в .htaccess - без разницы.
Как ставить версию в имя скрипта - зависит от Вашей шаблонной системы и, вообще, способа добавлять скрипты (стили и т.п.).
Например, при использовании даты модификации в качестве версии и шаблонизатора Smarty - ссылки можно ставить так:
<link href="{version src='/css/group.css'}" rel="stylesheet" type="text/css" />
Функция version добавляет версию:
function smarty_version($args){
$stat = stat($GLOBALS['config']['site_root'].$args['src']);
$version = $stat['mtime'];
echo preg_replace('!\.([a-z]+?)$!', ".v$version.\$1", $args['src']);
}
Результат на странице:
<link href="/css/group.v1234567890.css" rel="stylesheet" type="text/css" />
Чтобы избежать лишних вызовов stat, можно хранить массив со списком текущих версий в отдельной переменной
$versions['css'] = array(
'group.css' => '1.1',
'other.css' => '3.0',
}
В этом случае в HTML просто подставляется текущая версия из массива.
Можно скрестить оба подхода, и выдавать во время разработки версию по дате модификации - для актуальности, а в продакшн - версию из массива, для производительности.
Такой способ кеширования работает везде, включая Javascript, CSS, изображения, flash-ролики и т.п.
Он полезен всегда, когда документ изменяется, но в браузере всегда должна быть текущая актуальная версия.
|
Автор: tenshi, дата: 3 мая, 2008 - 10:59
#permalinkбраузеры не кэшируют вопросики только если не переданы заголовки регулирующие кэширование.
.ня
Автор: tenshi, дата: 3 мая, 2008 - 11:06
#permalinkда, и обратная сторона медали - необходимость грохать полностраничный кэш при изменении хотябы одного ресурса. плюс мы не можем экспортировать наши ресурсы на сторонние страницы ( чужие или генерируемые на машинках не имеющих быстрых способов получения актуальных версий ресурсов ).
.ня
Автор: tenshi, дата: 3 мая, 2008 - 11:14
#permalinkболее оптимальное кэширование:
устанавливаем время кэширования в несколько секунд и прячемся за проксёй, которая, соответственно, будет стучаться к нам не чаще чем раз в несколько секунд, а клиенту быстро отдавать 304 или новый контент.
и не надо изобретать велосипед...
.ня
Автор: dark57, дата: 19 мая, 2008 - 12:35
#permalinkНа медленном инете (wifi или gprs) или на просто глючном инете (например, p2p сеть задействовала тысящи запросов и плюс канал забит по максимуму) бывает так, что какой-нть форум загрузится, а css не может, хотя он 100% сидит в кеше. В результате форум отрисован в Times New Roman
. Иногда css таки подгружается через 5-10 секунд, а иногда так и остается.
Способ, описанный в статье, как раз избавляет от этого недуга.
Автор: oleg12 (не зарегистрирован), дата: 3 июня, 2008 - 19:39
#permalinkУ меня вопрос. я создаю страницу которая периодически обновляется, поэтому кэширование не желательно. но в сранице используется скрипт prototype.js который весит почти 100 кб. в несколько раз больше чем вся страница. можно ли как то на долго кешировать именно этот скрипт, а остальную страницу не кешировать?
Автор: tenshi, дата: 4 июня, 2008 - 21:13
#permalinkможно. копай в сторону http headers и .htaccess,
.ня
Автор: des (не зарегистрирован), дата: 9 августа, 2008 - 00:05
#permalinkПрописал этот код на сервере:
RewriteEngine on
# убирает версию, и заодно ставит переменную что файл версионный
RewriteRule ^/(.*\.)v[0-9.]+\.(css|js|gif|png|jpg)$ /$1$2 [L,E=VERSIONED_FILE:1]
# жестко кешируем версионные файлы
Header add "Expires" "Mon, 28 Jul 2014 23:30:00 GMT" env=VERSIONED_FILE
Header add "Cache-Control" "max-age=315360000" env=VERSIONED_FILE
Сервер ругается, что у Header должно быть всего три параметра: действие, заголовок и содержание заголовка. Исполнять не хочет.
У нас получается четыре параметра.
И что делать?
Автор: des (не зарегистрирован), дата: 10 августа, 2008 - 00:04
#permalinkДа вот в том-то и хрень, наверное... У хостера облазил всё - нет намёка на версию...
Только по переданным заголовкам увидел, что Apache 1.3.37...
У него, я так понял, не работает эта штука...
Автор: TaunT (не зарегистрирован), дата: 19 января, 2009 - 22:57
#permalinkоо... сколько ништяков!
в смысле полезной информации
Автор: tty01 (не зарегистрирован), дата: 7 февраля, 2009 - 19:06
#permalink> Аналогичным образом работает связка Last-Modified + If-Modified-Since
Браво, я бы сказал это самое краткое и понятное описание из того, что читал в сети.
Автор: JB (не зарегистрирован), дата: 10 мая, 2009 - 08:33
#permalinkЕще стоит рассмотреть изменение имени по хешу файла и кодирование его в 36-ричной системе (для краткости).
Достоинства:
- "версия" зависит от реального содержимого (т.е. возможен и возврат к старой версии скрипта с использованием старого кэша браузера);
- нет зависимости от времени изменения файла;
- увеличение безопасности (для запроса файла нужно знать его хеш, а не просто имя).
Недостатки:
- при переформатировании скрипта изменится его хеш (но эта ситуация (переформатирование), на мой взгляд, мало вероятна);
- при изменении только скрипта изменится и текст страницы (для исправления ошибок только в скрипте можно предусмотреть специальный механизм, но сомневаюсь, что это сильно нужно).
Автор: demimurych (не зарегистрирован), дата: 17 октября, 2009 - 15:27
#permalinkВсе вроде бы так да не так.
Например нажав в firefoxe кнопку reload firefox все равно шлет НА ВСЕ ЭЛЕМЕНТЫ ЗАПРОС забивая на кеширование. В ответ конечно получает 306 но тем не менее зачем то запросы шлет.
Автор: demimurych (не зарегистрирован), дата: 21 октября, 2009 - 20:56
#permalinkно и это еще не все.
достаточно закрыть тот же фаерфокс хотя бы на 20 минут. И он опять по новой выгребает ВСЕ.
Автор: mycoding, дата: 14 мая, 2011 - 20:05
#permalinkУ меня Жесткое кеширование не работает.
Каждый раз опять загружаются файлы.
Пишу в начале кода
header("content-type: text/css"); header("Expires: ".gmdate("D, d M Y H:i:s", time()+86400*365)." GMT"); header("Cache-Control: max-age="+86400*365); ob_start("ob_gzhandler");Автор: neonshark (не зарегистрирован), дата: 18 августа, 2011 - 10:27
#permalinkНебольшое исправление в правило:
RewriteRule ^/(.*\.)v[0-9\.]+\.(css|js|gif|png|jpg)$ /$1$2 [L,E=VERSIONED_FILE:1](добавлена косая черта '\' перед '.'). Позволит правильно обработать запросы типа my-js.v1.1.js, my-js.v1.1.1.js и т.п.
Автор: neonshark (не зарегистрирован), дата: 19 августа, 2011 - 08:35
#permalinkА статья суперская, все получилось и работает (я на VDS под Linux).
Автор: Paul_onclick, дата: 30 октября, 2011 - 12:06
#permalinkheader("Expires: ".gmdate("D, d M Y H:i:s", time()+86400*365)." GMT"); header("Cache-Control: max-age="+86400*365);А куда такие header'ы вписывать, если нужно, предположим, js-файл закэшировать? В сам файл с JS?
Автор: Mixa (не зарегистрирован), дата: 7 декабря, 2011 - 20:21
#permalinkДоброго времени суток!
Хочу сначала выразить огромную благодарность за проделаную работу на пути к просвитлению тьмы!
Вопрос (возможно я чет не до понял, а скорее всего так и есть):
мне нужно чтобы браузер не кешировал мое изображение в анимации, как это правильно прописать?
Автор: Хыиуду, дата: 5 мая, 2012 - 12:00
#permalinkА можно еще
href=css/style.css?ver=1.0
При изменении файла css в подключающем файле просто меняется номер версии, и браузер его обновляет без вопросов
Автор: Раед, дата: 12 мая, 2012 - 22:51
#permalinkА нельзя поменять порядок с помощью order?
Не всегда есть возможность использовать главные конфиг-файлы
Автор: blackswanny, дата: 1 ноября, 2012 - 11:40
#permalinkУ меня стоит следующая задача:
Есть статические js-файлы в хедере страницы. Развернуто веб-приложение из этой одной страницы (html), которое является лишь частью всего большого проекта на ASP.NET.
Необходимо, чтобы работало кеширование без запроса на сервер даже с проверкой модификации. Т.е. чтобы обновить приложение можно было, если почистить кеш браузера. При этом сервер IIS 7, который конфигурировать его нельзя. В ASP.NET тоже вклиниваться нежелательно.
Решение пока не нашел.
Автор: CoddX, дата: 17 декабря, 2012 - 12:17
#permalinkнарод, может кто подскажет как в ие 8 с помощью javascript чистить кеш?
или может у кого завалялась ссылочка на эту тему ), буду очень признателен
Автор: Горбунов (не зарегистрирован), дата: 24 сентября, 2014 - 19:27
#permalinkЗдравствуйте,
Вы не могли бы подсказать, возможно ли с помощю подобной функции
(может как либо измененной):
function smarty_version($args){
$stat = stat($GLOBALS['config']['site_root'].$args['src']);
$version = $stat['mtime'];
echo preg_replace('!\.([a-z]+?)$!', ".v$version.\$1", $args['src']);
}
дополнить версиями картинки (jpeg, pnp), что на странице?
Или если есть какая то другая?
Буду ОЧЕНЬ благодарен!