Лучшие практики кэширования. JavaScript кэширование Усовершенствованное кэширование страниц

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

Для наглядности приведу пример до:
function loadData(url, fn) { ajax.get(url, function (err, result) { //тут обрабатываем ответ и возвращаем результат var data = "..."; fn(null, data); }); }
и после:
var cache = {}; function loadCacheData(url, fn) { var cacheData = cache; //проверяем кэш if (cacheData) { fn(null, cacheData); } else { ajax.get(url, function (err, result) { //тут обрабатываем ответ и возвращаем результат var data = "..."; //сохраняем в кэш cache = data; fn(null, data); }); }

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

Это положило начало к написанию простой функции обертки которая превращала бы loadData в loadCacheData.
Выглядеть это должно примерно так:
function loadData(url, fn) { ajax.get(url, function (err, result) { //тут обрабатываем ответ и возвращаем результат var data = "..."; fn(null, data); }); } var loadCacheData = cache(loadData);
Полученная таким способом функция «loadCacheData» должна сама кэшировать результат, а так же предоставлять функционал по управлению кэша.

Через некоторое время удалось реализовать более менее работающую версию функции «cache». Все исходники доступны на github .

Что позволяет делать эта функция:

1. Сброс кэша.
var loadCacheData = cache(loadData); loadCacheData.clearCache();

2. Устанавливать время жизни для кэша. По умолчанию устанавливает на 1 год.
var loadCacheData = cache({ expire: 10 * 1000 }, loadData);

3. Установка максимального размера кэша (сколько ответов хранить в кэше). По умолчанию 10.
var loadCacheData = cache({ max: 10 }, loadData);

4. Выбор способа хранения кэш. По умолчанию «app».
app - хранить кэш в памяти.
local - хранить кэш в localStorage и доступен между всеми страницами сайта.
var loadCacheData = cache({ storage: "local" }, loadData);
При установке «local» опционально можно установить «cacheKey» - имя ключа в localStorage где храниться кэш.

5. Установка собственного хранилища для кэша.
var loadCacheData = cache({ storage: new MyCacheStorage() }, loadData);
В этом случае MyCacheStorage() должен реализовывать три метода:
«get(key, fn);» - метод выбирает результаты из кэша по ключу.
«set(key, val, fn);» - метод устанавливает значение к кэш.
«clear(fn);» - метод сбрасывает кэш.
«fn» - функция обратного вызова, принимающая первым аргументом «ошибку», вторым «результат» (если он есть) выполнения функции.

Использование «cache-js» накладывает одно ограничение, функции, результат которых необходимо «закэшировать», должны подчиняться правилу: «Последний параметр должен быть функцией обратного вызова!».

  • Сейчас функция «cache» добавляется в глобальную область, планируется вынести в отдельную
  • Добавить кэш с использованием sessionStorage
  • Глобальные опции, например что бы полностью отключить кэш при разработке
  • Поддержка старых браузеров. В данный момент используются методы parse/stringify объекта JSON, getItem/setItem/removeItem объекта localStorage
  • !? Порт на nodejs (под вопросом)

Примеры с использованием доступны в

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

Запрет кэширования страницы на HTML

Сделать это можно с помощью мета тегов. Сейчас мы разберем разные варианты запрета на кэширование.

Запрет на кэширование браузером и прокси-сервером

Запрет кэширования страницы, только браузером

Установка кэширования на определенное время, для браузера

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

Установка кэширования на определенное время, для прокси-сервера

Практически, то же самое, что и в предыдущем коде, только указание стоит конкретно для прокси-сервера.

Запретить кэширование страницы с помощью PHP

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

", date("H:i:s"), ""; ?>

Также, можно разрешать кэшировать на определенное время. Например, разрешим кэширование только на 1 час.

", date("H:i:s"), ""; ?>

Запретить кэширование страницы с помощью.htaccess

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

LoadModule expires_module modules/mod_expires.so LoadModule headers_module modules/mod_headers.so ... AddModule mod_expires.c AddModule mod_headers.c

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

# Заголовок Cache-Control Header append Cache-Control "no-store, no-cache, must-revalidate" # Заголовок Expires ExpiresActive On ExpiresDefault "now"

Важно заметить, что полный запрет кэширования, повышает нагрузку на сервер. Поэтому, играйтесь с этим осторожно! А лучше, установите определенное время, на которое можно кэшировать документы. Например, установим кэширование на 1 час:

# Заголовок Cache-Control Header append Cache-Control "public" # Заголовок Expires ExpiresActive On ExpiresDefault "access plus 1 hours"

Заключение

AlexSol высказал интересную идею, относительно улучшения аяксовой навигации. Идея замечательная. Но ее можно доработать. Как? Обратите внимание на пример, который он приводит.

При нажатии на одну ссылку, догружается один контент. При нажатии на вторую — другой. Но если снова нажать на первую, снова будет подгружаться первый контент с сервера! Разумнее было бы кэшировать эти данные в браузере и не посылать лишний раз гонца запрос серверу. Хорошо бы прибегнуть к кэшированию.

Как сделать кэширование в JavaScript? Об этом данная статья.

Кэширование, с помощью объекта

Метод, о котором хочу рассказать — это кэширование с помощью объекта JavaScript. Суть в том, что мы создаем хэш-массив объект, имена полей которого совпадают с именами локаций. Значения этих полей — это кэшируемые тексты.

//создаем глобальный объект
var CACHE = new Object();

Как записать в кэш что-то? Проще простого!

alert(CACHE.key);

Кроме того, нужно помнить, что объекты в JS - это по сути, ассоциативные массивы. То есть

CACHE.key == CACHE;

Вооружившись этим знанием, приступаем к построению нашего тестового приложения.

Структура html

Для начала, определимся с html. У меня получилось что-то такое:



jQurey cache ajax




1 |
2 |
3


Как видно, я решил оставил задел, чтобы сделать красивую навигацию по методу AlexSol-а. Делать я ее не буду, но прикрутить ее потом будет делом двух минут. Пока что, давайте узнаем, что делает функция getData?

var CACHE = new Object(); //создаем объект кэша

function getData(id_loc, url2load) {
if (CACHE) { //если в кэше еще нет нужных данных
//загружаем требуемый файл и вызываем функцию cache_n_go,
//которой передаем содержимое файла и id нажатой ссылки
$.get(url2load, function(data) {cache_n_go(data, id_loc)});
}
//если в кэше уже есть нужные нам данные
else {
//получаем данные из кэша
showRes(CACHE + "(из кэша)");
}
}

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

Теперь посмотрим что творится в функции cahce_n_go.

function cache_n_go(text, id_loc) {
CACHE = text;
showRes(text);
}

Ничего сверхъестественного в ней нет. Она кэширует полученый текст и отправляет его функции showRes, чтобы та его показала. Кстати, вот и она:

function showRes(text) {
var res = $("#res");
res.empty();
res.html(text);
}

Как видно, она принимает текст, опустошает блок для вывода контента, и выводит в него то, что ей передали. Вот и все.

Подключая внешние CSS и Javascript, мы хотим снизить до минимума лишние HTTP-запросы.

Для этого.js и.css файлы отдаются с заголовками, обеспечивающими надежное кеширование.

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

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

Простое кеширование ETag

Самый простой способ кеширования статических ресурсов - использование ETag .

Достаточно включить соответствующую настройку сервера (для Apache включена по умолчанию) - и к каждому файлу в заголовках будет даваться ETag - хеш, который зависит от времени обновления, размера файла и (на inode-based файловых системах) inode.

Браузер кеширует такой файл и при последующих запросах указывет заголовок If-None-Match с ETag кешированного документа. Получив такой заголовок, сервер может ответить кодом 304 - и тогда документ будет взят из кеша.

Выглядит это так:

Первый запрос к серверу (кеш чистый) GET /misc/pack.js HTTP/1.1 Host: сайт

Вообще, браузер обычно добавляет еще пачку заголовоков типа 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: сайт 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:

  1. сервер посылает дату последней модификации в заголовке Last-Modified (вместо ETag)
  2. браузер кеширует документ, и при следующем запросе того же документа посылает дату закешированной версии в заголовке If-Modified-Since (вместо If-None-Match)
  3. сервер сверяет даты, и если документ не изменился - высылает только код 304, без содержимого.

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

Умное кеширование. Версионность

Общий подход для версионности - в двух словах:

  1. Во все скрипты добавляется версия (или дата модификации). Например, http://сайт/my.js превратится в http://сайт/my.v1.2.js
  2. Все скрипты жестко кешируются браузером
  3. При обновлении скрипта версия меняется на новую: http://сайт/my.v2.0.js
  4. Адрес изменился, поэтому браузер запросит и закеширует файл заново
  5. Старая версия 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:

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

Большинство браузеров (Opera, Internet Explorer 6+, Safari) НЕ кешируют документы, если в адресе есть вопросительный знак, т.к считают их динамическими.

Именно поэтому мы добавляем версию в имя файла. Конечно, с такими адресами приходится использовать решение типа mod_rewrite, мы это рассмотрим дальше в статье.

P.S А вот Firefox кеширует адреса с вопросительными знаками..

Автоматическое преобразование имен

Разберем, как автоматически и прозрачно менять версии, не переименовывая при этом сами файлы.

Имя с версией -> Файл

Самое простое - это превратить имя с версией в оригинальное имя файла.

На уровне Apache это можно сделать mod_rewrite:

RewriteEngine on RewriteRule ^/(.*\.)v+\.(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+\.(css|js|gif|png|jpg)$ /$1$2 # жестко кешируем версионные файлы 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 - без разницы.

Автоматическое добавление версии в имя файла на HTML-странице

Как ставить версию в имя скрипта - зависит от Вашей шаблонной системы и, вообще, способа добавлять скрипты (стили и т.п.).

Например, при использовании даты модификации в качестве версии и шаблонизатора Smarty - ссылки можно ставить так:

Функция version добавляет версию:

Function smarty_version($args){ $stat = stat($GLOBALS["config"]["site_root"].$args["src"]); $version = $stat["mtime"]; echo preg_replace("!\.(+?)$!", ".v$version.\$1", $args["src"]); }

Результат на странице:

Оптимизация

Чтобы избежать лишних вызовов stat , можно хранить массив со списком текущих версий в отдельной переменной

$versions["css"] = array("group.css" => "1.1", "other.css" => "3.0", }

В этом случае в HTML просто подставляется текущая версия из массива.

Можно скрестить оба подхода, и выдавать во время разработки версию по дате модификации - для актуальности, а в продакшн - версию из массива, для производительности.

Применимость

Такой способ кеширования работает везде, включая Javascript, CSS, изображения, flash-ролики и т.п.

Он полезен всегда, когда документ изменяется, но в браузере всегда должна быть текущая актуальная версия.