Алгоритмы и задачи клиентской оптимизации

Уменьшение количества запросов

Разбить на страницы
Показывать лекцию целиком

4.1. Автоматическое объединение текстовых файлов

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

4.1.1. Объединение CSS-файлов

Несмотря на более простой и поистине академический синтаксис, CSS-файлы довольно сложно объединять в силу разных причин. Тут и различные атрибуты media (указывающие на устройства, для которых предназначен данный файл), и возможность сделать "вложенную" загрузку стилей при помощи @import и т. д. Для начала рассмотрим процесс получения ссылок и содержимого самих файлов из исходной структуры веб-страницы.

Получаем код

Если в CMS у нас предусмотрена возможность вставки CSS-файла как отдельного объекта в секцию head страницы, то это ограждает от множества проблем по "вычленению" этих объектов из готового HTML-кода. В противном случае нам придется использовать примерно следующий вариант:

/* регулярное выражение для нахождения всех
<link rel= "stylesheet"> и <style type="text/css">
внутри head-секции */
$regex =
"!(<link[^>]+rel\\s*=\\s*(\"stylesheet\"|'stylesheet'|stylesheet)
([^>]*)>|<style\\s+type\\s*=\\s*(\"text/css\"|'text/css'|text/css
)([^>]*)>(.*?)</style>)!is";
preg_match_all($regex, $this->head, $matches, PREG_SET_ORDER);
if (!empty($matches)) {
foreach($matches as $match) {
$file = array();
$file['tag'] = 'link';
$file['source'] = $match[0];
/* вырезаем из найденного куска HTML-кода обрамляющие теги,
чтобы идентифицировать внутренние стилевые правила */
$file['content'] =
preg_replace("/(<link[^>]+>|<style[^>]*>[\t\s\r\n]*|[\t\s\r\n]*<\
/style>)/i", "", $match[0]);
/* определяем все дополнительные атрибуты */
preg_match_all("@(type|rel|media|href)\s*=\s*(?:\"([^\"]+)\"|'([^
']+)'|([\s]+))@i", $match[0], $variants, PREG_SET_ORDER);
if(is_array($variants)) {
foreach($variants AS $variant_type) {
$variant_type[1] = strtolower($variant_type[1]);
$variant_type[2] = !isset($variant_type[2]) ?
(!isset($variant_type[3]) ?
$variant_type[4] :
$variant_type[3]) :
$variant_type[2];
switch ($variant_type[1]) {
/* выставляем источник для файла стилей */
case "href":
$file['file'] = trim($this->strip_querystring($variant_
type[2]));
$file['file_raw'] = $variant_type[2];
break;
default:
/* пропускаем media="all|screen" для предотвращения некорректного
поведения Safari при @media all{} или @media screen{} */
if ($variant_type[1] != 'media' || ($variant_type[1]
== 'media'  !preg_match("/all|screen/i",
$variant_type[2]))) {
$file[$variant_type[1]] = $variant_type[2];
}
break;
}
}
}
$this->initial_files[] = $file;
}
}

Подавая на вход данного алгоритма код секции head нашего документа ( $this->head ), на выходе мы получаем готовый массив $this->ini-tialfiles. Стоит сразу отметить, что в массиве для файлов стилей атрибут media не выставляется, если он равен all (в этом случае он просто бесполезен) либо screen (по умолчанию у нас все стилевые правила применяются для отображения сайтов на мониторах, поэтому данное значение также можно безболезненно опустить).

Разбираем вложенность

Получить ссылки на используемые файлы мало, нам необходимо полное содержимое этих файлов. Следует иметь в виду, что нам нужно распознать все внутренние конструкции @import (подключающие дополнительные файлы стилей) в порядке их появления в исходных файлах. Проще всего с данной проблемой может разобраться рекурсивная функция resolve_css_imports:

function resolve_css_imports($src) {
$content = file_get_contents($src);
/* удаляем из первоначального содержимого @import внутри
комментариев */
$content = preg_replace("!/\*\s*@import.*?\*/!is", "",
$content);
/* выбираем все @import */
preg_match_all('/@import\s*(url)?\s*\(?([^;]+?)\)?;/i',
$content, $imports, PREG_SET_ORDER);
if (is_array($imports)) {
foreach ($imports as $import) {
$src = false;
/* очищаем найденный путь к файлу от пробелов и кавычек */
if (isset($import[2])) {
$src = $import[2];
$src = trim($src, '\'" ');
}
if ($src) {
/* запускаем рекурсию для обнаруженного файла, чтобы разрешить
все @import уже внутри него */
$content = str_replace($import[0],
$this->resolve_css_imports($src), $content);
/* изменяем все пути для CSS-изображений и ресурсов (относительно
заданного файла) на абсолютные (относительно корня документа) */
$content = $this->resolve_relative_paths($src, $content);
}
}
}
return $content;
}

Задав полный путь к файлу стилей для функции resolve_css_imports, мы полностью разрешим все внутренние включения, чем сведем число HTTP-запросов к минимуму.

Объединяем

После того как мы разобрались с массивом файлов и научились получать полное их содержимое, нам нужно корректно их объединить. Как уже описывалось в книге "Разгони свой сайт" (http://speedupyourwebsite.ru/ books/speed-up-your-website/), для этого лучше всего применять конструкцию @media. Предположим, что в результирующем массиве у нас объект имеет следующий формат:

$this->initial_files = array(
array(
'content' => 'полное содержимое файла',
'media' => 'print|handheld|etc',
'file_raw' => 'исходный код файла в head-секции'
),
...
)

Тогда нам нужно просто объединить весь CSS-код в соответствие со спецификацией:

foreach ($this->initial_files as $file) {
if (!empty($file['media'])) {
$full_content .= '@media '. $file['media'] . '{';
}
$full_content .= $file['content'];
if (!empty($file['media'])) {
$full_content .= '}';
}
}

На выходе мы получим весь CSS-код, обнаруженный внутри секции head, объединенный в одну строку, которую можно записать в один кэши-рованный файл. Далее нужно, используя свойство fileraw, удалить исходные файлы и внутренний код из документа и вставить (например, сразу же после <head>) вызов этого кэшированного файла.

Минимизируем

А что, если мы хотим не только объединить файлы, но и уменьшить их в размере? Gzip-компрессию здесь рассматривать не будем: она достаточно тривиальна в реализации (и может сводиться к нескольким правилам в конфигурационном файле сервера). Нам более интересен вопрос уменьшения CSS-кода в соответствии с CSS-спецификацией. Здесь разумнее всего воспользоваться одним из трех путей.

  • Набор простых регулярных выражений (он был описан еще в книге "Разгони свой сайт"). Ниже приведен его код на Perl.
    $data = ? s!\/\*(.*?)\*\/!!g; # удаляем комментарии
    $data = ? s!\s+! !g; # сжимаем пробелы
    $data = ? s!\} !}\n!g; # добавляем переводы строки
    $data = ? s!\n$!!; # удаляем последний перевод
    строки
    $data = ? s! \{ ! {!g; # удаляем лишние пробелы
    внутри скобок
    $data = ? s!; \}!}!g; # удаляем лишние пробелы и
    синтаксис внутри скобок
  • CSS Tidy (http://csstidy.sourceforge.net/) — наиболее мощная библиотека для разбора CSS-правил. Для ее использования необходимо загрузить ее в папку проекта, внести изменения в настройки по умолчанию (находятся в файле class.csstidy.php ) и осуществить минимизацию простыми вызовами:
    $css = new csstidy();
    $css->load_template($root_dir . 'css.template.tpl');
    $css->parse($css_code);
    echo $css->print->formatted();

    При этом для максимального сжатия лучше использовать следующий шаблон ( css.template.tpl ):

    |{||{|||;|}||}||{||

  • YUI Compressor (http://developer.yahoo.com/yui/compressor/). Эта библиотека требует установленной Java на сервере и запускается еще проще. Необходимо из командной строки выполнить:
    java -jar yuicompressor.jar -o output.css input.css

    Результат произведенных действий будет сохранен в файле output.css.

  • Полный код для CSS Tidy и все аспекты практической реализации можно почерпнуть из исходного кода Web Optimizer (http://www.web-optimizer.ru/).

    Сразу стоит оговориться, что в Intenet Explorer (по 8-ю версию включительно) есть проблема с отображением более 4096 (по сведениям из MSDN, http://msdn.microsoft.com/en-us/library/aa358796(VS.85).aspx) CSS-селекторов из одного файла (и ограничение в 32 на число @import). При разработке грамотного процесса объединения CSS-файлов этот момент стоит учитывать.

    4.1.2. Объединение JavaScript-файлов

    Для JavaScript-файлов весь описанный механизм повторяется, за исключением небольших деталей.

    Получаем код

    Во-первых, получать код мы будем уже немного другим методом, и для нас будет несущественен атрибут media:

    $regex =
    "!<script[^>]+type\\s*=\\s*(\"text/javascript\"|'text/javascript'
    |text/javascript)([^>]*)>(.*?</script>)!is";
    preg_match_all($regex, $this->head, $matches, PREG_SET_ORDER);
    if (!empty($matches)) {
    foreach($matches as $match) {
    $file = array();
    $file['tag'] = 'script';
    $file['source'] = $match[0];
    /* вырезаем из найденного куска HTML-кода обрамляющие теги,
    чтобы идентифицировать внутренние скрипты */
    $file['content'] = preg_replace("/(<script[^>]*>
    [\t\s\r\n]*|[\t\s\r\n]*<\/script>)/i", "", $match[0]);
    $file['file'] = '';
    preg_match_all("@(type|src)\s*=\s*(?:\"([^\"]+)\"|'([^']+)'
    |([\s]+))@i", $match[0], $variants, PREG_SET_ORDER);
    if(is_array($variants)) {
    foreach($variants AS $variant_type) {
    $variant_type[1] = strtolower($variant_type[1]);
    $variant_type[2] = !isset($variant_type[2]) ?
    (!isset($variant_type[3]) ? $variant_type[4] :
    $variant_type[3]) : $variant_type[2];
    switch ($variant_type[1]) {
    case "src":
    $file['file'] =
    trim($this->strip_querystring($variant_type[2]));
    $file['file_raw'] = $variant_type[2];
    break;
    default:
    $file[$variant_type[1]] = $variant_type[2];
    break;
    }
    }
    }
    $this->initial_files[] = $file;
    }
    }

    Объединяем

    Тут нас ждет еще одно отличие: разные куски JavaScript-кода лучше объединять через точку с запятой с переводом строки. Ибо предыдущая часть кода может не оканчиваться на точку с запятой, потому мы обязаны как-то отделить ее от последующей.

    Далее в ходе объединения было установлено, что файлы библиотек для визуального форматирования кода (в силу своей сложности) мало приспособлены к объединению с другими файлами. Поэтому рекомендуется при объединении избегать следующих файлов: tinymce.js и fckeditor.js. Во всем остальном механизм абсолютно тот же самый, что и для CSS-файлов (за исключением отсутствия необходимости разрешить @import и необходимости заменять пути для фоновых изображений и ресурсов).

    Минимизируем

    Для минимизации JavaScript-кода лучше всего использовать уже имеющиеся на рынке решения: JSMin (http://www.crockford.com/ javascript/jsmin.html, который портирован в том числе и на PHP) или YUI Compressor (http://developer.yahoo.com/yui/compressor/). Про последний уже было написано чуть выше (параметры для запуска те же самые). В случае с JSMin все тоже довольно просто: нам нужно загрузить последнюю версию (http://code.google.com/p/jsmin-php/), подключить ее и просто вызвать минимизацию заданного файла:

    require 'jsmin-1.1.1.php';
    echo JSMin::minify(file_get_contents('example.js'));

    Стоит также упомянуть, что классический JSMin не поддерживает условную компиляцию для IE. Поэтому тут нужно воспользоваться модифицированным решением (например, из исходных кодов Web Optimizer).

    4.1.3. Заключение

    Объединение текстовых файлов способно значительно ускорить загрузку вашего сайта, не причиняя вреда качеству разработки (вы можете разрабатывать отдельно и в автоматическом режиме выкладывать на сайт уже готовые версии файлов). Как показывают данные с webo.in, на произвольном сайте в Рунете используется 2,7 файлов стилей (средний размер — 5,5 Кб) и 5 JavaScript-файлов (средний размер — 15 Кб). Простое их объединение позволит выиграть 0,5-1 с при загрузке страницы. А минимизация (вместе с gzip-сжатием, уменьшающим размер на 85%) — еще 60 Кб, что составит 0,6 с при скорости подключения 100 Кб/с.

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

    4.2. Алгоритм разбора и сбора CSS Sprites

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

    4.2.1. Обзор технологии

    CSS Sprites, на самом деле, — всего лишь расширенное использование технологии background, заложенной еще в спецификации CSS1. Все, до чего додумалось прогрессивное человечество за эти годы, — это множественный фон у элементов (как он будет совместим с концепцией CSS Sprites, еще придется проверить). Основные свойства, которые мы используем для задания фонового изображения:

  • background-image — основная "рабочая лошадка". Именно за ней будущее в виде data:URI, который в конце концов победит CSS Sprites. Но произойдет это еще не скоро;
  • background-repeat — вторая не менее важная составляющая при использовании фонового изображения. Ведь задавая no-repeat для данного свойства, мы намеренно подчеркиваем, что допустимо использование CSS Sprites для "склейки" изображений (по умолчанию используется repeat);
  • background-position — "волшебная палочка" для CSS Sprites, позволяющая спрятать или показать определенные части фонового изображения.
  • Кроме заявленных свойств также есть еще несколько (например, background-color), но они к спрайтам имеют посредственное отношение. Однако стоит добавить к нашим объектам, с помощью которых мы будем формировать карты изображений, размеры (в относительных или абсолютных единицах) и отступы (padding). Это позволит точнее разделить изображения по группам и корректно расположить их в финальном комбинированном изображении.

    4.2.2. Прикладные тонкости

    Естественно, рассматривая набор возможных эффектов при использовании фоновых изображений, стоит отметить следующие:

  • Объект, полностью заполненный фоновым изображением.

    Здесь основную роль играют конечные размеры объекта (разумеется, если изображение не повторяется по всем осям сразу: в таком случае использовать его для CSS Sprites не представляется возможным). Довольно часто фон под объектом может меняться в зависимости от каких-либо условий (преднамеренный акцент или действия со стороны пользователя), но для логики создания CSS Sprites это несущественно. Здесь же можно выделить три подслу-чая: соответствующих неповторяющемуся фону и повторяющемуся по оси X или Y.

  • Фоновое изображение заполняет не весь предоставленный ему объем (либо размеры объекта не заданы, либо заданы в относительных — em, % — единицах). Тут нам необходимо прикреплять повторяющееся изображение "в конец" спрайта, чтобы на той части объекта, что осталась без фонового изображения, не проявлялось никаких дефектов. Либо (в случае no-repeat) расположить изображения "лесенкой" (это особенно актуально в случае фона для элементов списка). Стоит отметить, что в зависимости от значения background-position CSS Sprites здесь могут быть как возможны, так и невозможны в принципе (например, в случае 100% 100%). Тут можно выделить еще несколько случаев, различающихся по background-position, background-repeat и линейными размерами блока.
  • Изображение является анимированным. Поскольку далее речь пойдет о применении PNG- и JPEG-изображений для CSS Sprites, то анимированные изображения придется сразу выбросить из рассмотрения: поддержка анимированных PNG-изобра-жений находится сейчас на самом зачаточном уровне в браузерах.
  • Все описанные примеры можно более четко структурировать по следующим группам:

  • background-repeat: no-repeat, background-position: абсолютные числа и заданы линейные абсолютные размеры.
  • background-repeat: no-repeat, background-position: абсолютные числа, линейные размеры не заданы или заданы в относительных единицах.
  • background-repeat: repeat-x, задана высота элемента.
  • background-repeat: repeat-x, высота элемента не задана.
  • background-repeat: repeat-y, задана ширина элемента.
  • background-repeat: repeat-y, ширина элемента не задана.
  • background-repeat: no-repeat, background-position: 100% 0, задана высота элемента (в абсолютных единицах).
  • background-repeat: no-repeat, background-position: 0 100%, задана ширина элемента (в абсолютных единицах).
  • background-repeat: no-repeat, background-position: 100% 0, высота элемента не задана (или задана в относительных единицах).
  • background-repeat: no-repeat, background-position: 0 100%, ширина элемента не задана (или задана в относительных единицах).
  • background-repeat: repeat.
  • background-repeat: repeat-x или background-repeat: repeat-y, размеры элемента указаны в относительных единицах.
  • background-repeat: no-repeat, background-position: относительные единицы.
  • изображение является анимированным GIF-файлом.
  • Глядя на эту спецификацию, становится в общем понятно, в каком направлении двигаться для автоматизации создания CSS Sprites. Стоит только отметить, что при использовании одного и того же изображения многими CSS-селекторами нужно отследить background-position и устранить изначальные CSS Sprites, задействованные в стилях. Процесс получения изображений из готовых CSS Sprites в автоматическом режиме достаточно сложен и может быть применим только на локальных проектах.

    4.2.3. Практическое решение: CSS Tidy

    Далее речь пойдет уже об инструменте Auto Sprites (http://sprites.in/), который был положен в основу разработки Web Optimizer (http://www.web-optimizer.ru/). После описанных выше умозаключений оставались чисто технические вопросы: как все это закодировать и как отладить полученное решение.

    Для начала нам нужно разобрать все дерево CSS-правил, потом отобрать из них относящиеся к фоновым изображениям и линейным размерам объектов, а уже потом произвести над ними требуемые действия. Идеально для этой задачи подходит CSS Tidy (http://csstidy.sourceforge.net/), который был замечательно испробован, протестирован и улучшен после интеграции на сотнях реальных сайтов. CSS Tidy представляет собой набор PHP-библиотек, которые могут быть включены в произвольный проект для каких-либо действий над заданным набором стилевых правил.

    Ниже приводится простой алгоритм выделения из массива CSS-правил нужных нам свойств фона на языке PHP:

    /* $this — объект класса css_sprites */
    /* сначала создадим новый объект CSS Tidy, используя заданный
    $css_code*/
    $this->css = new csstidy();
    $this->css->parse($css_code);
    /* определим CSS-свойство для элементов без фона, */
    /* чтобы не переопределить его в ходе преобразований */
    $this->none = 'none!important';
    /* далее мы переберем весь массив на предмет сначала
    @media-конструкций*/
    foreach ($this->css->css as $import => $token) {
    /* создадим для каждого заданного @media свой массив правил */
    $this->media[$import] = array();
    /* а затем и самих CSS-селекторов */
    foreach ($token as $tags => $rule) {
    /* получим для каждого набора селекторов массив CSS-свойств и их
    значений */
    foreach ($rule as $key => $value) {
    /* выделим из всех свойств только относящиеся к фону */
    if (preg_match("/background/", $key)) {
    /* переопределим "выключенный" фон, чтобы не затронуть */
    /* в ходе преобразований */
    if ($key == 'background'  $value == 'none') {
    $this->css->css[$import][$tags]['background'] =
    $this->none;
    }
    /* теперь для каждого отдельного CSS-селектора */
    foreach (split(",", $tags) as $tag) {
    /* создаем отдельный объект */
    if (!empty($this->media[$import][$tag])) {
    $this->media[$import][$tag] = array();
    }
    if ($key == 'background') {
    /* получаем массив фоновых свойств из CSS-свойства background */
    $background = $this->css->optimise-
    >dissolve_short_bg($value);
    foreach ($background as $bg => $property) {
    /* пропускаем свойства, заданные по умолчанию */
    if (
    /* в частности, для background-position */
    !($bg == 'background-position' 
    ($property == '0 0 !important' ||
    $property == 'top left !important' ||
    $property == '0 0' ||
    $property == 'top left')) 
    /* для background-origin */
    !($bg == 'background-origin' 
    ($property == 'padding !important' |
    $property == 'padding')) 
    /* для background-color */
    !($bg == 'background-color' 
    ($property == 'transparent !important' ||
    $property == 'transparent')) 
    /* для background-clip */
    !($bg == 'background-clip' 
    ($property == 'border !important' ||
    $property == 'border')) 
    /* для background-attachement */
    !($bg == 'background-attachment' 
    ($property == 'scroll !important' ||
    $property =='scroll')) 
    /* для background-size */
    !($bg == 'background-size' 
    ($property == 'auto !important' ||
    $property == 'auto')) 
    /* и для background-repeat */
    !($bg == 'background-repeat' 
    ($property == 'repeat !important' ||
    $property == 'repeat'))) {
    /* Переопределяем background-image, если оно не задано */
    if ($bg == 'background-image' 
    ($property == 'none !important' ||
    $property == 'none')) {
    $property = $this->none;
    }
    /* и дополняем background-position, вместо left выставляем
    left center, вместо right — right center,
    и ряд других исправлений */
    if ($bg == 'background-position') {
    $property =
    $this->compute_background_position
    ($property);
    }
    /* В конце выставляем полученные значения для массива правил,
    определяющих исходные фоновые картинки */
    $this->media[$import][$tag][$bg] = $property;
    }
    }
    /* Если у нас задано детальное CSS-свойство, то просто его на-
    значаем */
    } else {
    /* Дополняем background-position, вместо left выставляем left
    center,
    а вместо right — right center,
    также меняем местами bottom right и некоторые другие случаи */
    if ($key == 'background-position') {
    $value =
    $this->compute_background_position($value);
    }
    /* и выставляем "исправленные" свойства для массива CSS-правил,
    пропуская свойства по умолчанию */
    if ($key != 'background-position' || $value != '0 0') {
    $this->media[$import][$tag][$key] = $value;
    }
    }
    }
    }
    /* завершаем цикл перебора исходных CSS-правил */
    }
    }
    }

    4.2.4. Практическое решение: сборка изображений

    Дальше начинается самое интересное: как нам вышеописанные группы "склеивать"? Для этого используется следующий алгоритм:

  • Изображения, для которых позиционирование задано в процентах и которые находятся внутри контейнеров с абсолютными размерами, корректируются, чтобы фоновая позиция была задана в аболютных единицах.
  • repeat-x изображения (группа 3) объединяются все вместе по вертикали. Попутно правится ширина фоновых изображений (приводится к наименьшему общему кратному). В самое начало такого файла добавляются no-repeat изображения, подходящие по ширине (группа 1). Далее в самый низ файла записывается 1 изображение из группы 4. Больше 1 все равно никуда не войдет, поскольку нам нужно обеспечить свободное пространство ниже заявленной высоты фонового изображения. Это часто бывает нужно для плавного перетекания градиента в ровный цвет: градиент вставляется фоновой картинкой, которая заканчивается на фоновом цвете, а дальше продолжается уже этот цвет, заданный через background-color.
  • Производятся абсолютно аналогичные действия с repeat-y.
  • Далее изображения из группы 7 объединяются по вертикали (0 100% означает, что фон должен быть прижат к правому краю элемента, соответственно, весь спрайт будет "прижат" к правому своему краю).
  • Аналогично с группой 8 — прижимаем все к низу. Естественно, что для всех групп мы учитываем первоначальное значение background-position.
  • Рассчитываем позиционирование для изображений группы 1 (для этого подойдет и просто перебор отсортированных по площади или сумме квадратов измерений изображений: готовим матрицу, в которую пытаемся "вписать" очередное изображение; если не получается, то матрицу увеличиваем). При вписывании ради экономии процессорного времени можно проверять только 9 точек (8 по краям изображения и 1 в центре), чтобы убедиться, что на месте изображения еще ничего нет.
  • Строим "лесенку" из изображений второй группы. Лесенку лучше строить с низа уже созданного спрайта из предыдущего пункта: тогда легко найти минимальный размер "дырки" между двумя группами изображений, чтобы сдвинуть "лесенку" вверх (и потом, возможно, влево). Конечно, поиск наиболее оптимального расположения — непростая задача. Но ее можно решить и в достаточно грубом приближении, которое описано выше.
  • Итоговое изображение из пункта 4 прикрепляется к правому верхнему углу нашего сложного изображения (результат действия пунктов 6 и 7). Так как у каждого такого изображения задана допустимая высота и все они находятся вне "рабочей" зоны остальных no-repeat изображений, никаких рудиментов не возникнет.
  • Аналогичным образом поступаем с изображением из пункта 5 — его располагаем в нижнем левом углу нашего спрайта.
  • На выходе мы получаем 3 спрайта со всеми возможными случаями. В среднем размеры таких спрайтов будут лишь незначительно превосходить (если вообще будут) аналогичные "ручные" аналоги (в том числе за счет использования PNG). Естественно, можно в автоматическом режиме пропустить их через pngcrush или jpegtran. Стоит также предусмотреть, в каком виде будут создаваться полноцветные изображения: JPEG или PNG (последние больше по размеру, но гарантируют отсутствие потерь качества).
  • При расчете позиции картинки в конечном спрайте может помочь следующая схема:

    (рис 4.1) Схема позиционирования фоновой картинки относительно элемента

    В силу громоздкости решения (в нем более 1500 строк кода) в полном объеме в данной книге оно не приводится, однако все описанные шаги уже применены в Web Optimizer (http://www.web-optimizer.ru/) (веб-приложении для автоматизации клиентской оптимизации). Одна из финальных версий алгоритма работает для инструмента Auto Sprites (http://sprites.in/), а с исходным текстом можно ознакомиться в SVN (http://code.googLe.eom/p/ web-optimizator/source/browse/trunk/Libs/ php/css.sprites.php).

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

    4.3. CSS Sprites и data:URI, или Microsoft и весь остальной мир

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

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

  • несемантическая верстка в случае использования сложных спрай-тов. Это приводит к дополнительному времени создания документа (особенно существенно может быть для "старых" браузеров). (Более подробно о влиянии DOM-дерева на процесс загрузки страницы рассказывается в следующей лекции);
  • невозможность комбинирования нескольких осей повторения. Это не очень существенно, поскольку все использование CSS Sprites (как было описано в предыдущем разделе) можно свести к трем основным изображениям;
  • тяжесть изменения картинки в случае сложной геометрии;
  • отображение неверного фона при масштабировании.
  • 4.3.1. data:URI

    Есть ли выход из этого положения? Да, есть. Это технология data:URI, которая позволяет включать фоновые изображения прямо в CSS-файл в base64-виде. Плюсы данного подхода очевидны: не нужно "склеивать" несколько картинок в один файл, есть возможность объединять различные оси и анимированные с обычными изображениями, полностью отделить содержание (семантику документа) от его представления (оформления и дизайна), и т. д.

    Но есть и ложка дегтя: IE вплоть до версии 7 не поддерживает data:URI. IE8 — уже да, но все остальные IE — нет. Что делать?

    4.3.2. mhtml

    Нам на помощь приходит технология mhtml (MIME HTML), которую поддерживает по умолчанию только IE (почти в полной мере) и Opera (начиная с версии 9.0). Она позволяет включать base64-данные в CSS-файл в виде комментариев. В этом случае сам файл выступает в двух ипостасях: как таблица стилей и как хранилище фоновых картинок.

    Если мы объединим эту технологию с data:URI, то все будет хорошо. Правда?

    4.3.3. Проблема 1: долгая предзагрузка

    В случае включения фоновых картинок прямо в CSS-файл последний заметно увеличивается в размере (даже при использовании gzip-сжатия). Это значительно увеличивает время предзагрузки (если фоновых картинок больше 10—15 Кб), и пользователь дольше видит белый экран. Опять все плохо. Как быть?

    Возможным выходом из сложившейся ситуации может стать подключение CSS-файла с фоновыми картинками по комбинированному событию window.onload, что вынесет загрузку элементов дизайна в ту область, где она изначально находилась: на стадию полной загрузки страницы или даже в пост-загрузку. В данном случае мы получаем полную аналогию метода CSS Sprites, только без заявленных выше неудобств.

    4.3.4. Проблема 2: выключенный JavaScript

    Описанный выше прием позволит облегчить загрузку только пользователей с включенным (или поддерживаемым) JavaScript (их порядка 9899%). Однако в ряде проектов это может быть недостаточно. Для оставшихся пользователей мы можем через <noscript> подключить нужный нам файл (и конкретно для них замедлить предзагрузку) или поместить вызов этого файла перед </body> (что в ряде случаев может быть аналогично подключению стилей в <head> ).

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

    4.3.5. Проблема 3: Safari и window.onload

    Из-за того, что Safari отличается алгоритмом обновления страницы (что позволяет значительно ускорить отображение самих страниц), для этого браузера не удается загрузить дополнительные стили после отображения первоначальной картинки (на стадии полной загрузки страницы). В этом случае Safari блокирует отрисовку картинки на экран и ожидает загрузку нового файла стилей.

    На данный момент для Safari мы можем безболезненно загружать дополнительные файлы стилей только по полному событию window.onload.

    4.3.6. Проблема 4: Microsoft, IE7 и Windows Vista

    В результате проведенных исследований удалось установить, что в связи с проблемами безопасности в Vista mhtml-технология для отображения фоновых изображений не поддерживается. В этом случае единственным выходом будет подключение конкретно для этого браузера (через JavaScript) общего файла (с использованием CSS Sprites). Для пользователей IE7@Vista с отключенным JavaScript у нас нет другой альтернативы, чем загружать файл только с CSS Sprites.

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

    4.4. Автоматизация кроссбраузерного решения для data:URI

    (рис 4.2) Логотип duris.ru: Data:URI [CSS] Sprites. Источник: duris.ru

    Многим профессиональным веб-разработчикам известны приемы оптимизации сайтов. Одним из способов оптимизации является применение CSS Sprites. Этим же разработчикам известно, какие существуют трудности с формированием, сборкой и пересборкой стандартных спрайтов. При использовании стандартных спрайтов полностью автоматизировать сборку для всех случаев проблематично, из-за специфики свойств background в CSS. Иногда камнем преткновения является свойство repeat фоновой картинки.

    4.4.1. Data:URI CSS

    Существует альтернативный вариант генерации CSS-спрайтов — на основе схемы data:URI. Данный подход интересен тем, что максимально минимизируется количество обращений к серверу, и самое важное — можно полностью автоматизировать процесс сборки и перегенерации спрайтов. Для полной автоматизации процесса оптимизации работы сайтов и сборки CSS-спрайтов на основе data:URI схемы была разработана специальная библиотека.

    Мучения со стандартным подходом применения CSS-спрайтов, а именно, трудности модернизации и, в некоторых случаях, сложности оптимальной компоновки заставили искать альтернативный вариант оптимизации загрузки изображений. Далее речь пойдет об использовании комбинированного метода: data:URI + mhtml. Более детально процесс создания изображений в этом формате описан в пятой главе книги "Разгони свой сайт".

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

    Новый подход генерации CSS-спрайтов на основе data:URI решили назвать Data URI Sprites — DURIS.ru. Название немного необычное — но зато уникальное и хорошо запоминается.

    4.4.2. Что это?

    В первую очередь это полностью автоматический анализ и сборка CSS-спрайтов на основе data:URI.

    Некоторые характеристики работы DURIS:

  • загрузка и анализ всех внутренних ( <style> ) и внешних ( <link> ) стилей;
  • выделение background-image в отдельный внешний стиль;
  • загрузка и кодирование в base64 всех изображений, которые найдены в стилях;
  • оптимизация правил с повторяющимися background-image в стилях;
  • удаление CSS-правил с отсутствующими на сервере изображениями (устраняет лишние ненужные запросы);
  • специальное подключение data:URI спрайтов для всех браузеров и отдельно для IE6, IE7 Vista (более детально в FAQ, duris.ru/faq/).
  • 4.4.3. Зачем это надо?

    Современный подход создания CSS-спрайтов:

  • позволяет безболезненно вносить коррективы в изображения и верстку;
  • дает возможность свести к минимуму число запросов к серверу для получения информации, которая относится к оформлению страницы;
  • позволяет использовать полностью семантическую верстку;
  • устраняет проблемы масштабирования для фоновых изображений;
  • объединяет изображения разных типов и осей повторения;
  • все создается в автоматическом режиме.
  • 4.4.4. IE и другие

    В ходе разработки реализации data:URI спрайтов были проанализированы наиболее часто встречающие варианты CSS-правил. Также был учтен всеми любимый браузер IE. Кому еще не известно: IE не поддерживает до версии 8 технологию data:URI. Однако для него существует альтернативный вариант реализации спрайтов — на основе mhtml-технологии. Другими словами, на данный момент мы имеем полный спектр решений для всех современных браузеров (99% процентов всех используемых браузеров). Но, как всегда, "ложку дегтя" подкидывает Microsoft. Во время тестирования найдена ошибка mhtml-технологии в Vista IE7 — а именно, браузер IE7 в ОС Vista при кэшировании mhtml-файла отказывается показывать изображения (это связано с малодокументированными проблемами безопасности при использовании mhtml в Vista IE7). Если сделать файл некэшируемым, то все работает, как и должно работать, но в случае с кэшированием фоновые изображения не отображаются.

    В общем, Microsoft все же сделал так, чтобы "цепь разорвалась". На текущий момент для браузера IE7 в ОС Vista реализация DURIS работает не совсем так, как задумывалось изначально. В подключение стилей внедрен алгоритм проверки IE7 Vista и в случае обнаружения такового фоновые картинки подключаются стандартным путем. Статистика показывает, что пользователей, которые используют IE7 под Vista, около 5% процентов. В любом случае, мы уже знаем, что в IE8 нормально реализована data^RI-технология.

    4.4.5. Основные достоинства

    Выделим два наиболее важных достоинства использования современного подхода генерации CSS-спрайтов.

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

    На сегодняшний день мы имеем стабильную бета-версию DURIS, которая отрабатывает все передаваемые ей CSS-файлы. Отрабатываются также специфические правила, такие как filter:AlphaImageLoader, !important. Ядро DURIS разработано на языке Java и является самодостаточным (т. е. не зависит от сайта). Предполагается, что после получения релиз-кандидата исходный код ядра будет выложен в открытый доступ под OpenSource-лицензией. Ядро работает с командной строки наподобие того, как работает YUI Compressor. Это позволит удобно интегрировать автоматическую генерацию CSS-спрайтов в свои проекты. Кто программирует на Java, при желании сможет напрямую вызывать методы ядра DURIS.

    4.4.7. Итого

    Разработанный метод/алгоритм автоматической генерации CSS-спрай-тов основе data:URI уникален в своем роде и не имеет мировых аналогов. На сайте выложен FAQ (http://duris.ru/faq/), в котором детально описано, что и как работает. Если что не понятно — задаем вопросы в комментариях.

    В роли главного разработчика данного решения выступил Руслан Си-ницкий (aka sirus, http://fullajax/#:developers).

    4.4.8. Будущее

    При появлении поддержки в IE8 схемы data:URI (стоит все же помнить об ограничении в 32 Кб, http://msdn.microsoft.com/en-us/library/cc848897%28VS.85%29.aspx) разработанный подход становится довольно перспективным, теперь все современные браузеры поддерживают такие спрайты.

    Детально ознакомиться с принципами генерации и подключения data:URI CSS-спрайтов можно в разделе FAQ (http://duris.ru/faq/). Если у вас возникли вопросы или предложения по работе сайта или алгоритма в целом, вы можете оставить свои пожелания на этом сайте в форме обратной связи.

    Стоит также понимать, что наиболее эффективные методы клиентской оптимизации будут использовать комбинированные решения (как, например, это реализовано в Web Optimizer). Часть фоновых изображений, которые сложно или нерационально "склеивать" в CSS Sprites и которые невелики по размеру, мы можем включить прямо в CSS-файл. Размер его при этом увеличится незначительно.

    Большие же изображения (больше 24 Кб) мы не можем включать из принципов кроссбраузерности, они могут формировать CSS Sprites и этим не только уменьшать общее время загрузки, но и формировать наиболее оптимальную визуальную скорость загрузки сайта у конечного пользователя. При этом часть картинок (включенных через комбинированный data:URI) появится сразу же, а часть (включенных через CSS Sprites) — через некоторое время, так как они будут представлены отдельными файлами.

    4.5. Автоматизация кэширования

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

    4.5.1. Кэширование на клиентском уровне

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

    Статические ресурсы без сжатия

    Форсирование кэширования для статических ресурсов может выполняться без сжатия. В данном случае мы ничем не рискуем, выставляя не только максимальное время кэширования, но и предлагая кэшировать ресурсы на локальных прокси-серверах (директива Cache-Control: public ). Для PHP у нас получится следующий код (в Expires прописана дата на 10 лет вперед относительно текущего времени на сервере):

    <?php
    header("Cache-Control: public, max-age=315360000");
    header("Expires: Mon, 01 Jul 2019 00:00:00");
    ?>

    В случае выставления директив для Apache:

    <FilesMatch \.(bmp|png|gif|jpe?g|ico|swf|flv|pdf|tiff)$>
    Header append Cache-Control public
    ExpiresActive On
    ExpiresDefault "access plus 10 years"
    <FilesMatch>

    И в случае nginx:

    location ?* ^.+\.(bmp|gif|jpg|jpeg|png|swf|tiff|swf|flv)$ {
    expires 10y;
    header set Cache-Control public;
    }

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

    Статические ресурсы со сжатием

    Все отличие от предыдущего случая заключается в том, что разрешить локальным прокси-серверам кэшировать такие ресурсы (обычно это CSS-, JavaScript- или ICO-файлы) нельзя. Даже больше: нам нужно запретить кэширование сжатых версий файлов, чтобы избежать выдачи сжатого файла тем пользователям, которые сжатия не поддерживают. Код для PHP:

    <?php
    header("Cache-Control: private, max-age=315360000");
    header("Expires: Mon, 01 Jul 2019 00:00:00");
    ?>

    Для Apache:

    <FilesMatch \.(css|js|ico)$>
    Header append Cache-Control private
    ExpiresActive On
    ExpiresDefault "access plus 10 years"
    </FilesMatch>

    И для nginx:

    location ~* ^.+\.(css|js|ico)$ {
    expires 10y;
    header set Cache-Control private;
    }

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

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

    Запрет кэширования динамических ресурсов

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

    Для того чтобы запретить кэширование во всех браузерах HTML-документов, нужно написать с помощью PHP (в Expires прописано текущее время на сервере):

    header("Expires: Wed, 01 July 2009 00:00:00");
    header("Cache-Control: no-store, no-cache, must-revalidate,
    private");
    header("Pragma: no-cache");

    Для Apache:

    <FilesMatch \.(php|phtml|shtml|html|xml|htm)$>
    ExpiresActive Off
    Header append Cache-Control "no-store, no-cache,
    must-revalidate, private"
    Header append Pragma "no-cache"
    </FilesMatch>

    Для nginx:

    location ?* ^.+\.(php|phtml|shtml|html|xml|htm)$ {
    expires 0;
    header set Cache-Control "no-store, no-cache, must-revalidate,
    private";
    header set Pragma "no-cache";
    }

    4.5.2. Условное кэширование

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

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

    Существует два способа обеспечить условное кэширование: при помощи заголовков Last-Modified + If-Modified-Since и ETag + If-None-Match. Первый заголовок в паре относится к ответу со стороны сервера, второй — к запросу со стороны клиента, который уже получил соответствующий ответ сервера и теперь желает проверить, изменилось ли что-нибудь с той поры.

    При помощи пары Last-Modified мы можем установить соответствие ресурса только по времени его изменения. ETag (англ. Entity Tag, метка сущности) предполагает более широкую сферу применения: мы можем для ресурса назначить уникальную строку, зависящую не только от времени изменения, но и от других параметров (например, передается данный файл в сжатом виде или нет).

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

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

    /* получаем содержимое документа, например, из закэшированного
    файла */
    $content = @file_get_contents($file);
    /* вычисляем уникальную метку, зависящую от содержания */
    $hash = md5($content);
    /* проверяем соответствие вычисленной и переданной с клиента
    меток */
    if ((isset($_SERVER['HTTP_IF_NONE_MATCH']) 
    stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) == '"' . $hash .
    '"') ||
    (isset($_SERVER['HTTP_IF_MATCH']) 
    stripslashes($_SERVER['HTTP_IF_MATCH']) == '"' . $hash . '"')) {
    /* в случае совпадения меток передаем 304-ответ */
    header ("HTTP/1.0 304 Not Modified");
    /* и не передаем содержимое документа */
    header ("Content-Length: 0");
    exit();
    }
    /* во всех остальных случаях выставляем заголовок ETag */
    header("ETag: \"" . $hash . "\"");
    /* и отдаем полностью содержимое документа */
    echo $content;

    4.5.3. Сброс кэша

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

    Строка запроса

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

    Рассмотрим следующий вызов CSS-файла:

    <link rel="stylesheet" href="/css/main.css?20090701" type="text/css"/>

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

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

    <?php
    /* получаем метку времени, равную времени изменения файла */
    $timestamp = filemtime($file);
    /* выводим ссылку на файл в HTML-документе */
    echo '<link rel="stylesheet" href="/css/main.css?". $timestamp .
    "' type="text/css"/>';
    ?>

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

    Физическое имя файла

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

    RewriteRule ~(.*)(\.v[0-9]+)?\.(css|js)$ $1.$2 [QSA,L]

    Какой оно несет смысл? А тот, что при указании в HTML-документе ссылки на файл

    main.layout.v123456.css

    сервер отдаст физический файл

    main.layout.css

    Таким образом мы элегантно обходим проблему прокси-серверов одной строкой в конфигурации сервера. Соответствующий PHP-код будет выглядеть так:

    <?php
    /* получаем метку времени, равную времени изменения файла */
    $timestamp = filemtime($file);
    /* выводим ссылку на файл в HTML-документе */
    echo '<link rel="stylesheet" href="/css/main.css.v". $timestamp .
    "' type="text/css"/>';
    ?>

    Однако, как и в предыдущем случае, мы все равно обращаемся к файловой системе.

    Создание хэша

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

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

    $hash = '';
    foreach ($this->initial_files as $file) {
    $hash .= $file['file_raw'];
    }
    $new_file_name = md5($hash);

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

    Использование разделенной памяти

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

  • подключение библиотек разделяемой памяти (APC, eAccelerator, memcache);
  • возможность управлять состоянием кэша (редактирование проверяемых файлов через веб-интерфейс, частичный сброс кэша либо полный сброс закэшированных файлов). На примере APC описанный алгоритм выглядит следующим образом:
    <?php
    /* удаляем (ставим время истечения равным 1 с) из APC запись
    относительно */
    /* текущего файла, при изменении каких-либо включенных в него
    файлов */
    if ($changed) {
    apc_store($new_file_name, apc_fetch($new_file_name), 1);
    }
    …
    /* при выдаче закэшированного файла проверяем, нужно ли
    его пересобирать */
    $mtime = apc_fetch($new_file_name);
    if (!$mtime) {
    …
    /* если нужно, то при создании файла записываем текущее время
    в APC */
    $mtime = $_SERVER['REQUEST_TIME'];
    echo '<link rel="stylesheet" href="/css/main.css?". $mtime .
    "' type="text/css"/>';
    apc_store($new_file_name, $mtime);
    } else {
    /* если нет, то у нас уже получено время изменения файла,
    которое можно использовать для метки кэша */
    echo '<link rel="stylesheet" href="/css/main.css?". $mtime .
    "' type="text/css"/>';
    }
    ?>
  • Как можно видеть, предложенный алгоритм весьма прост (и может сводиться к простой процедуре очистки кэша, когда мы для всех созданных комбинированных файлов форсируем пересборку), но позволяет полностью избежать обращения к файловой системе (или ее кэшу) при действиях по клиентской оптимизации страницы.

    Таким образом, кэширование на клиентском уровне может ускорить загрузку последующих страниц (или посещений) вашего сайта на 500-1000% (в 5-10 раз), а правильное управление кэшированием гарантирует вам, что информация,получаемая пользователями, всегда будет актуальной.

    4.5.4. Кэширование на серверном уровне

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

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

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

    Отдаем закэшированный документ

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

    Для более детального примера давайте рассмотрим следующий код, отвечающий за простейшее серверное кэширование, из Web Optimizer:

    /* проверяем, можем ли отдать закэшированный документ */
    if (!empty($this->cache_me)) {
    /* переводим адрес документа в реальное имя файла */
    $this->uri = $this->convert_request_uri();
    $file = $this->options['page']['cachedir'] . '/' . $this->uri;
    /* проверяем, существует ли файл и достаточно ли он актуальный */
    if (is_file($file) 
    $_SERVER['REQUEST_TIME'] - filemtime($file) <
    $this->options['page']['cache_timeout']) {
    $content = @file_get_contents($file);
    /* проверяем, сжат ли файл */
    if (!empty($this->options['page']['gzip']) 
    substr($content, 0, 8) ==
    "\x1f\x8b\x08\x00\x00\x00\x00\x00") {
    /* если сжат, то выставляем соответствующие заголовки */
    $this->set_gzip_header();
    }
    /* отдает закэшированное содержимое */
    echo $content;
    /* и закрываем PHP-процес */
    die();
    }
    }

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

    Создаем закэшированный документ

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

    /* определяем, нужно ли нам сохранять закэшированную версию
    документа */
    if (!empty($this->cache_me)) {
    /* формируем имя файла */
    $file = $options['cachedir'] . '/' . $this->uri;
    /* проверяем, есть ли такой файл и не устарел ли он */
    if (!is_file($file) ||
    $_SERVER['REQUEST_TIME'] - filemtime($file) >
    $options['cache_timeout']) {
    /* записываем новое содержимое в файл */
    $fp = @fopen($file, "a");
    if ($fp) {
    /* блокируем файл от конкурентных попыток записи */
    @flock($fp, LOCK_EX);
    /* удаляем содержимое и перемещаемся на начало файла */
    @ftruncate($fp, 0);
    @fseek($fp, 0);
    @fwrite($fp, $this->content);
    @fclose($fp);
    }
    }
    }

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

    4.5.5. Кэширование XHR-запросов

    Данный раздел написан после прочтения заметки Steve Souders "F5 and XHR deep dive", посвященной вопросам кэширования XHR-ресурсов.

    Оказывается, что любые данные, полученные при помощи AJAX, никогда не будут обновлены в IE прежде истечения срока действия кэша, даже если вы форсируете обновление ( Ctrl+F5 ). Единственный путь обновить эти данные — это вручную удалить их из кэша.

    Если вы нажимаете Перезагрузку (F5), IE перезапросит все ресурсы (даже с неистекшим сроком действия кэша), за исключением XHR. Это может вызвать большое недоумение среди разработчиков при тестировании, но меня заинтересовало, какие еще проблемы существуют в этом направлении. Будет ли поведение аналогичным во всех остальных основных браузерах? Что произойдет, если срок давности кэша будет в прошлом или заголовок Expires вообще не будет выставлен? Будет ли какой-либо эффект от добавления Cache-Control max-age (который переписывает заголовок Expires)?

    Проводим тестирование

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

  • Вариант с Expires в прошлом добавляет в заголовки ответа Expires, который содержит дату на 30 дней ранее текущей, и Cache-Control с max-age=0.
  • Вариант без Expires вообще не выставляет никаких заголовков Expires или Cache-Control.
  • Вариант с Expires в будущем добавляет в заголовки ответа Expires, который содержит дату на 30 позже текущей, и Cache-Control с max-age=2592000.
  • Ниже в таблице приведены результаты тестирования этой страницы в основных браузерах. Также там записано, перезапрашивался ли XHR-ре-сурс или был прочитан из кэша, а если перезапрашивался, то с каким кодом HTTP-статуса.

    Ниже приведены соображения на тему того, что происходит при нажатии F5.

  • Все браузеры перезапрашивают и картинку, и внешний скрипт (это имеет смысл).
  • Все браузеры перезапрашивают XHR-ресурс, если срок действия кэша находится в прошлом (это тоже имеет смысл: браузер знает, что закэшированный XHR-ресурс устарел).
  • Если кэшируется XHR, что происходит при нажатии F5?
    Expires в прошлом Без Expires Expires в будущем
    Chrome 2 304 304 304
    Firefox 3.5 304 304 304
    IE 7 304 кэш кэш
    IE 8 304 кэш кэш
    Opera 10 304 кэш 304
    Safari 4 200 200 200
  • Единственное различие в поведении замечено в тот момент, когда для XHR-ресурса нет заголовка Expires, или же Expires выставлен в будущее. IE 78 не перезапрашивают XHR-ресурс, если нет Expires или Expires выставлен в будущее, даже при нажатии Ctrl+F5. Opera 10 не перезапрашивает XHR-ресурс, если нет Expires (эквивалента для Ctrl+F5 в Opera найти не удалось).
  • И Opera 10, и Safari 4 перезапрашивают favicon.ico во всех случаях (это весьма расточительно).
  • Safari 4 не посылает заголовок If-Modified-Since во всех случаях. В результате ответ всегда приходит со статус-кодом 200 и включает запрашиваемый ресурс полностью. Это верно как для XHR-ре-сурса, так и для картинок и внешних скриптов (это выглядит неоптимально и отличается от поведения других браузеров).
  • Выводы

    Ниже резюмированы рекомендации для веб-разработчиков при работе с XHR-ресурсами.

  • Разработчики должны выставлять срок действия кэша для XHR-ресурсов или в прошлом, или в будущем, чтобы предотвратить расхождения в поведении браузеров, когда Expires вообще не выставлен.
  • Если XHR-ресурсы вообще не должны быть закэшированы, разработчикам стоит выставлять дату изменения ресурса в прошлое. Это давняя проблема с различным поведением браузеров при наличии закэшированных копий определенных ресурсов, и касается она не только XHR-запросов. Например, не всегда пользователи будут перезагружать страницу — они могут на нее попасть, просто переходя по ссылкам. В этом случае браузер выдаст им закэ-шированные версии XHR. Для форсирования сброса кэша мы можем выставлять, например, дополнительный GET-параметр, и это будет работать для всех браузеров и всех прокси-серверов. Более подробно вопросы сброса кэша описаны ранее в этом разделе.
  • Если XHR-запросы желательно кэшировать, то разработчики должны назначить срок истечения кэша в будущем. При тестировании в IE 78 разработчикам придется не забывать очищать кэш, чтобы проверить действие Перезагрузки (F5).
  • Страницы:

    4.1. Автоматическое объединение текстовых файлов

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

    4.1.1. Объединение CSS-файлов

    Несмотря на более простой и поистине академический синтаксис, CSS-файлы довольно сложно объединять в силу разных причин. Тут и различные атрибуты media (указывающие на устройства, для которых предназначен данный файл), и возможность сделать "вложенную" загрузку стилей при помощи @import и т. д. Для начала рассмотрим процесс получения ссылок и содержимого самих файлов из исходной структуры веб-страницы.

    Получаем код

    Если в CMS у нас предусмотрена возможность вставки CSS-файла как отдельного объекта в секцию head страницы, то это ограждает от множества проблем по "вычленению" этих объектов из готового HTML-кода. В противном случае нам придется использовать примерно следующий вариант:

    /* регулярное выражение для нахождения всех
    <link rel= "stylesheet"> и <style type="text/css">
    внутри head-секции */
    $regex =
    "!(<link[^>]+rel\\s*=\\s*(\"stylesheet\"|'stylesheet'|stylesheet)
    ([^>]*)>|<style\\s+type\\s*=\\s*(\"text/css\"|'text/css'|text/css
    )([^>]*)>(.*?)</style>)!is";
    preg_match_all($regex, $this->head, $matches, PREG_SET_ORDER);
    if (!empty($matches)) {
    foreach($matches as $match) {
    $file = array();
    $file['tag'] = 'link';
    $file['source'] = $match[0];
    /* вырезаем из найденного куска HTML-кода обрамляющие теги,
    чтобы идентифицировать внутренние стилевые правила */
    $file['content'] =
    preg_replace("/(<link[^>]+>|<style[^>]*>[\t\s\r\n]*|[\t\s\r\n]*<\
    /style>)/i", "", $match[0]);
    /* определяем все дополнительные атрибуты */
    preg_match_all("@(type|rel|media|href)\s*=\s*(?:\"([^\"]+)\"|'([^
    ']+)'|([\s]+))@i", $match[0], $variants, PREG_SET_ORDER);
    if(is_array($variants)) {
    foreach($variants AS $variant_type) {
    $variant_type[1] = strtolower($variant_type[1]);
    $variant_type[2] = !isset($variant_type[2]) ?
    (!isset($variant_type[3]) ?
    $variant_type[4] :
    $variant_type[3]) :
    $variant_type[2];
    switch ($variant_type[1]) {
    /* выставляем источник для файла стилей */
    case "href":
    $file['file'] = trim($this->strip_querystring($variant_
    type[2]));
    $file['file_raw'] = $variant_type[2];
    break;
    default:
    /* пропускаем media="all|screen" для предотвращения некорректного
    поведения Safari при @media all{} или @media screen{} */
    if ($variant_type[1] != 'media' || ($variant_type[1]
    == 'media'  !preg_match("/all|screen/i",
    $variant_type[2]))) {
    $file[$variant_type[1]] = $variant_type[2];
    }
    break;
    }
    }
    }
    $this->initial_files[] = $file;
    }
    }

    Подавая на вход данного алгоритма код секции head нашего документа ( $this->head ), на выходе мы получаем готовый массив $this->ini-tialfiles. Стоит сразу отметить, что в массиве для файлов стилей атрибут media не выставляется, если он равен all (в этом случае он просто бесполезен) либо screen (по умолчанию у нас все стилевые правила применяются для отображения сайтов на мониторах, поэтому данное значение также можно безболезненно опустить).

    Разбираем вложенность

    Получить ссылки на используемые файлы мало, нам необходимо полное содержимое этих файлов. Следует иметь в виду, что нам нужно распознать все внутренние конструкции @import (подключающие дополнительные файлы стилей) в порядке их появления в исходных файлах. Проще всего с данной проблемой может разобраться рекурсивная функция resolve_css_imports:

    function resolve_css_imports($src) {
    $content = file_get_contents($src);
    /* удаляем из первоначального содержимого @import внутри
    комментариев */
    $content = preg_replace("!/\*\s*@import.*?\*/!is", "",
    $content);
    /* выбираем все @import */
    preg_match_all('/@import\s*(url)?\s*\(?([^;]+?)\)?;/i',
    $content, $imports, PREG_SET_ORDER);
    if (is_array($imports)) {
    foreach ($imports as $import) {
    $src = false;
    /* очищаем найденный путь к файлу от пробелов и кавычек */
    if (isset($import[2])) {
    $src = $import[2];
    $src = trim($src, '\'" ');
    }
    if ($src) {
    /* запускаем рекурсию для обнаруженного файла, чтобы разрешить
    все @import уже внутри него */
    $content = str_replace($import[0],
    $this->resolve_css_imports($src), $content);
    /* изменяем все пути для CSS-изображений и ресурсов (относительно
    заданного файла) на абсолютные (относительно корня документа) */
    $content = $this->resolve_relative_paths($src, $content);
    }
    }
    }
    return $content;
    }

    Задав полный путь к файлу стилей для функции resolve_css_imports, мы полностью разрешим все внутренние включения, чем сведем число HTTP-запросов к минимуму.

    Объединяем

    После того как мы разобрались с массивом файлов и научились получать полное их содержимое, нам нужно корректно их объединить. Как уже описывалось в книге "Разгони свой сайт" (http://speedupyourwebsite.ru/ books/speed-up-your-website/), для этого лучше всего применять конструкцию @media. Предположим, что в результирующем массиве у нас объект имеет следующий формат:

    $this->initial_files = array(
    array(
    'content' => 'полное содержимое файла',
    'media' => 'print|handheld|etc',
    'file_raw' => 'исходный код файла в head-секции'
    ),
    ...
    )

    Тогда нам нужно просто объединить весь CSS-код в соответствие со спецификацией:

    foreach ($this->initial_files as $file) {
    if (!empty($file['media'])) {
    $full_content .= '@media '. $file['media'] . '{';
    }
    $full_content .= $file['content'];
    if (!empty($file['media'])) {
    $full_content .= '}';
    }
    }

    На выходе мы получим весь CSS-код, обнаруженный внутри секции head, объединенный в одну строку, которую можно записать в один кэши-рованный файл. Далее нужно, используя свойство fileraw, удалить исходные файлы и внутренний код из документа и вставить (например, сразу же после <head>) вызов этого кэшированного файла.

    Минимизируем

    А что, если мы хотим не только объединить файлы, но и уменьшить их в размере? Gzip-компрессию здесь рассматривать не будем: она достаточно тривиальна в реализации (и может сводиться к нескольким правилам в конфигурационном файле сервера). Нам более интересен вопрос уменьшения CSS-кода в соответствии с CSS-спецификацией. Здесь разумнее всего воспользоваться одним из трех путей.

  • Набор простых регулярных выражений (он был описан еще в книге "Разгони свой сайт"). Ниже приведен его код на Perl.
    $data = ? s!\/\*(.*?)\*\/!!g; # удаляем комментарии
    $data = ? s!\s+! !g; # сжимаем пробелы
    $data = ? s!\} !}\n!g; # добавляем переводы строки
    $data = ? s!\n$!!; # удаляем последний перевод
    строки
    $data = ? s! \{ ! {!g; # удаляем лишние пробелы
    внутри скобок
    $data = ? s!; \}!}!g; # удаляем лишние пробелы и
    синтаксис внутри скобок
  • CSS Tidy (http://csstidy.sourceforge.net/) — наиболее мощная библиотека для разбора CSS-правил. Для ее использования необходимо загрузить ее в папку проекта, внести изменения в настройки по умолчанию (находятся в файле class.csstidy.php ) и осуществить минимизацию простыми вызовами:
    $css = new csstidy();
    $css->load_template($root_dir . 'css.template.tpl');
    $css->parse($css_code);
    echo $css->print->formatted();

    При этом для максимального сжатия лучше использовать следующий шаблон ( css.template.tpl ):

    |{||{|||;|}||}||{||

  • YUI Compressor (http://developer.yahoo.com/yui/compressor/). Эта библиотека требует установленной Java на сервере и запускается еще проще. Необходимо из командной строки выполнить:
    java -jar yuicompressor.jar -o output.css input.css

    Результат произведенных действий будет сохранен в файле output.css.

  • Полный код для CSS Tidy и все аспекты практической реализации можно почерпнуть из исходного кода Web Optimizer (http://www.web-optimizer.ru/).

    Сразу стоит оговориться, что в Intenet Explorer (по 8-ю версию включительно) есть проблема с отображением более 4096 (по сведениям из MSDN, http://msdn.microsoft.com/en-us/library/aa358796(VS.85).aspx) CSS-селекторов из одного файла (и ограничение в 32 на число @import). При разработке грамотного процесса объединения CSS-файлов этот момент стоит учитывать.

    4.1.2. Объединение JavaScript-файлов

    Для JavaScript-файлов весь описанный механизм повторяется, за исключением небольших деталей.

    Получаем код

    Во-первых, получать код мы будем уже немного другим методом, и для нас будет несущественен атрибут media:

    $regex =
    "!<script[^>]+type\\s*=\\s*(\"text/javascript\"|'text/javascript'
    |text/javascript)([^>]*)>(.*?</script>)!is";
    preg_match_all($regex, $this->head, $matches, PREG_SET_ORDER);
    if (!empty($matches)) {
    foreach($matches as $match) {
    $file = array();
    $file['tag'] = 'script';
    $file['source'] = $match[0];
    /* вырезаем из найденного куска HTML-кода обрамляющие теги,
    чтобы идентифицировать внутренние скрипты */
    $file['content'] = preg_replace("/(<script[^>]*>
    [\t\s\r\n]*|[\t\s\r\n]*<\/script>)/i", "", $match[0]);
    $file['file'] = '';
    preg_match_all("@(type|src)\s*=\s*(?:\"([^\"]+)\"|'([^']+)'
    |([\s]+))@i", $match[0], $variants, PREG_SET_ORDER);
    if(is_array($variants)) {
    foreach($variants AS $variant_type) {
    $variant_type[1] = strtolower($variant_type[1]);
    $variant_type[2] = !isset($variant_type[2]) ?
    (!isset($variant_type[3]) ? $variant_type[4] :
    $variant_type[3]) : $variant_type[2];
    switch ($variant_type[1]) {
    case "src":
    $file['file'] =
    trim($this->strip_querystring($variant_type[2]));
    $file['file_raw'] = $variant_type[2];
    break;
    default:
    $file[$variant_type[1]] = $variant_type[2];
    break;
    }
    }
    }
    $this->initial_files[] = $file;
    }
    }

    Объединяем

    Тут нас ждет еще одно отличие: разные куски JavaScript-кода лучше объединять через точку с запятой с переводом строки. Ибо предыдущая часть кода может не оканчиваться на точку с запятой, потому мы обязаны как-то отделить ее от последующей.

    Далее в ходе объединения было установлено, что файлы библиотек для визуального форматирования кода (в силу своей сложности) мало приспособлены к объединению с другими файлами. Поэтому рекомендуется при объединении избегать следующих файлов: tinymce.js и fckeditor.js. Во всем остальном механизм абсолютно тот же самый, что и для CSS-файлов (за исключением отсутствия необходимости разрешить @import и необходимости заменять пути для фоновых изображений и ресурсов).

    Минимизируем

    Для минимизации JavaScript-кода лучше всего использовать уже имеющиеся на рынке решения: JSMin (http://www.crockford.com/ javascript/jsmin.html, который портирован в том числе и на PHP) или YUI Compressor (http://developer.yahoo.com/yui/compressor/). Про последний уже было написано чуть выше (параметры для запуска те же самые). В случае с JSMin все тоже довольно просто: нам нужно загрузить последнюю версию (http://code.google.com/p/jsmin-php/), подключить ее и просто вызвать минимизацию заданного файла:

    require 'jsmin-1.1.1.php';
    echo JSMin::minify(file_get_contents('example.js'));

    Стоит также упомянуть, что классический JSMin не поддерживает условную компиляцию для IE. Поэтому тут нужно воспользоваться модифицированным решением (например, из исходных кодов Web Optimizer).

    4.1.3. Заключение

    Объединение текстовых файлов способно значительно ускорить загрузку вашего сайта, не причиняя вреда качеству разработки (вы можете разрабатывать отдельно и в автоматическом режиме выкладывать на сайт уже готовые версии файлов). Как показывают данные с webo.in, на произвольном сайте в Рунете используется 2,7 файлов стилей (средний размер — 5,5 Кб) и 5 JavaScript-файлов (средний размер — 15 Кб). Простое их объединение позволит выиграть 0,5-1 с при загрузке страницы. А минимизация (вместе с gzip-сжатием, уменьшающим размер на 85%) — еще 60 Кб, что составит 0,6 с при скорости подключения 100 Кб/с.

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

    4.2. Алгоритм разбора и сбора CSS Sprites

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

    4.2.1. Обзор технологии

    CSS Sprites, на самом деле, — всего лишь расширенное использование технологии background, заложенной еще в спецификации CSS1. Все, до чего додумалось прогрессивное человечество за эти годы, — это множественный фон у элементов (как он будет совместим с концепцией CSS Sprites, еще придется проверить). Основные свойства, которые мы используем для задания фонового изображения:

  • background-image — основная "рабочая лошадка". Именно за ней будущее в виде data:URI, который в конце концов победит CSS Sprites. Но произойдет это еще не скоро;
  • background-repeat — вторая не менее важная составляющая при использовании фонового изображения. Ведь задавая no-repeat для данного свойства, мы намеренно подчеркиваем, что допустимо использование CSS Sprites для "склейки" изображений (по умолчанию используется repeat);
  • background-position — "волшебная палочка" для CSS Sprites, позволяющая спрятать или показать определенные части фонового изображения.
  • Кроме заявленных свойств также есть еще несколько (например, background-color), но они к спрайтам имеют посредственное отношение. Однако стоит добавить к нашим объектам, с помощью которых мы будем формировать карты изображений, размеры (в относительных или абсолютных единицах) и отступы (padding). Это позволит точнее разделить изображения по группам и корректно расположить их в финальном комбинированном изображении.

    4.2.2. Прикладные тонкости

    Естественно, рассматривая набор возможных эффектов при использовании фоновых изображений, стоит отметить следующие:

  • Объект, полностью заполненный фоновым изображением.

    Здесь основную роль играют конечные размеры объекта (разумеется, если изображение не повторяется по всем осям сразу: в таком случае использовать его для CSS Sprites не представляется возможным). Довольно часто фон под объектом может меняться в зависимости от каких-либо условий (преднамеренный акцент или действия со стороны пользователя), но для логики создания CSS Sprites это несущественно. Здесь же можно выделить три подслу-чая: соответствующих неповторяющемуся фону и повторяющемуся по оси X или Y.

  • Фоновое изображение заполняет не весь предоставленный ему объем (либо размеры объекта не заданы, либо заданы в относительных — em, % — единицах). Тут нам необходимо прикреплять повторяющееся изображение "в конец" спрайта, чтобы на той части объекта, что осталась без фонового изображения, не проявлялось никаких дефектов. Либо (в случае no-repeat) расположить изображения "лесенкой" (это особенно актуально в случае фона для элементов списка). Стоит отметить, что в зависимости от значения background-position CSS Sprites здесь могут быть как возможны, так и невозможны в принципе (например, в случае 100% 100%). Тут можно выделить еще несколько случаев, различающихся по background-position, background-repeat и линейными размерами блока.
  • Изображение является анимированным. Поскольку далее речь пойдет о применении PNG- и JPEG-изображений для CSS Sprites, то анимированные изображения придется сразу выбросить из рассмотрения: поддержка анимированных PNG-изобра-жений находится сейчас на самом зачаточном уровне в браузерах.
  • Все описанные примеры можно более четко структурировать по следующим группам:

  • background-repeat: no-repeat, background-position: абсолютные числа и заданы линейные абсолютные размеры.
  • background-repeat: no-repeat, background-position: абсолютные числа, линейные размеры не заданы или заданы в относительных единицах.
  • background-repeat: repeat-x, задана высота элемента.
  • background-repeat: repeat-x, высота элемента не задана.
  • background-repeat: repeat-y, задана ширина элемента.
  • background-repeat: repeat-y, ширина элемента не задана.
  • background-repeat: no-repeat, background-position: 100% 0, задана высота элемента (в абсолютных единицах).
  • background-repeat: no-repeat, background-position: 0 100%, задана ширина элемента (в абсолютных единицах).
  • background-repeat: no-repeat, background-position: 100% 0, высота элемента не задана (или задана в относительных единицах).
  • background-repeat: no-repeat, background-position: 0 100%, ширина элемента не задана (или задана в относительных единицах).
  • background-repeat: repeat.
  • background-repeat: repeat-x или background-repeat: repeat-y, размеры элемента указаны в относительных единицах.
  • background-repeat: no-repeat, background-position: относительные единицы.
  • изображение является анимированным GIF-файлом.
  • Глядя на эту спецификацию, становится в общем понятно, в каком направлении двигаться для автоматизации создания CSS Sprites. Стоит только отметить, что при использовании одного и того же изображения многими CSS-селекторами нужно отследить background-position и устранить изначальные CSS Sprites, задействованные в стилях. Процесс получения изображений из готовых CSS Sprites в автоматическом режиме достаточно сложен и может быть применим только на локальных проектах.

    4.2.3. Практическое решение: CSS Tidy

    Далее речь пойдет уже об инструменте Auto Sprites (http://sprites.in/), который был положен в основу разработки Web Optimizer (http://www.web-optimizer.ru/). После описанных выше умозаключений оставались чисто технические вопросы: как все это закодировать и как отладить полученное решение.

    Для начала нам нужно разобрать все дерево CSS-правил, потом отобрать из них относящиеся к фоновым изображениям и линейным размерам объектов, а уже потом произвести над ними требуемые действия. Идеально для этой задачи подходит CSS Tidy (http://csstidy.sourceforge.net/), который был замечательно испробован, протестирован и улучшен после интеграции на сотнях реальных сайтов. CSS Tidy представляет собой набор PHP-библиотек, которые могут быть включены в произвольный проект для каких-либо действий над заданным набором стилевых правил.

    Ниже приводится простой алгоритм выделения из массива CSS-правил нужных нам свойств фона на языке PHP:

    /* $this — объект класса css_sprites */
    /* сначала создадим новый объект CSS Tidy, используя заданный
    $css_code*/
    $this->css = new csstidy();
    $this->css->parse($css_code);
    /* определим CSS-свойство для элементов без фона, */
    /* чтобы не переопределить его в ходе преобразований */
    $this->none = 'none!important';
    /* далее мы переберем весь массив на предмет сначала
    @media-конструкций*/
    foreach ($this->css->css as $import => $token) {
    /* создадим для каждого заданного @media свой массив правил */
    $this->media[$import] = array();
    /* а затем и самих CSS-селекторов */
    foreach ($token as $tags => $rule) {
    /* получим для каждого набора селекторов массив CSS-свойств и их
    значений */
    foreach ($rule as $key => $value) {
    /* выделим из всех свойств только относящиеся к фону */
    if (preg_match("/background/", $key)) {
    /* переопределим "выключенный" фон, чтобы не затронуть */
    /* в ходе преобразований */
    if ($key == 'background'  $value == 'none') {
    $this->css->css[$import][$tags]['background'] =
    $this->none;
    }
    /* теперь для каждого отдельного CSS-селектора */
    foreach (split(",", $tags) as $tag) {
    /* создаем отдельный объект */
    if (!empty($this->media[$import][$tag])) {
    $this->media[$import][$tag] = array();
    }
    if ($key == 'background') {
    /* получаем массив фоновых свойств из CSS-свойства background */
    $background = $this->css->optimise-
    >dissolve_short_bg($value);
    foreach ($background as $bg => $property) {
    /* пропускаем свойства, заданные по умолчанию */
    if (
    /* в частности, для background-position */
    !($bg == 'background-position' 
    ($property == '0 0 !important' ||
    $property == 'top left !important' ||
    $property == '0 0' ||
    $property == 'top left')) 
    /* для background-origin */
    !($bg == 'background-origin' 
    ($property == 'padding !important' |
    $property == 'padding')) 
    /* для background-color */
    !($bg == 'background-color' 
    ($property == 'transparent !important' ||
    $property == 'transparent')) 
    /* для background-clip */
    !($bg == 'background-clip' 
    ($property == 'border !important' ||
    $property == 'border')) 
    /* для background-attachement */
    !($bg == 'background-attachment' 
    ($property == 'scroll !important' ||
    $property =='scroll')) 
    /* для background-size */
    !($bg == 'background-size' 
    ($property == 'auto !important' ||
    $property == 'auto')) 
    /* и для background-repeat */
    !($bg == 'background-repeat' 
    ($property == 'repeat !important' ||
    $property == 'repeat'))) {
    /* Переопределяем background-image, если оно не задано */
    if ($bg == 'background-image' 
    ($property == 'none !important' ||
    $property == 'none')) {
    $property = $this->none;
    }
    /* и дополняем background-position, вместо left выставляем
    left center, вместо right — right center,
    и ряд других исправлений */
    if ($bg == 'background-position') {
    $property =
    $this->compute_background_position
    ($property);
    }
    /* В конце выставляем полученные значения для массива правил,
    определяющих исходные фоновые картинки */
    $this->media[$import][$tag][$bg] = $property;
    }
    }
    /* Если у нас задано детальное CSS-свойство, то просто его на-
    значаем */
    } else {
    /* Дополняем background-position, вместо left выставляем left
    center,
    а вместо right — right center,
    также меняем местами bottom right и некоторые другие случаи */
    if ($key == 'background-position') {
    $value =
    $this->compute_background_position($value);
    }
    /* и выставляем "исправленные" свойства для массива CSS-правил,
    пропуская свойства по умолчанию */
    if ($key != 'background-position' || $value != '0 0') {
    $this->media[$import][$tag][$key] = $value;
    }
    }
    }
    }
    /* завершаем цикл перебора исходных CSS-правил */
    }
    }
    }

    4.2.4. Практическое решение: сборка изображений

    Дальше начинается самое интересное: как нам вышеописанные группы "склеивать"? Для этого используется следующий алгоритм:

  • Изображения, для которых позиционирование задано в процентах и которые находятся внутри контейнеров с абсолютными размерами, корректируются, чтобы фоновая позиция была задана в аболютных единицах.
  • repeat-x изображения (группа 3) объединяются все вместе по вертикали. Попутно правится ширина фоновых изображений (приводится к наименьшему общему кратному). В самое начало такого файла добавляются no-repeat изображения, подходящие по ширине (группа 1). Далее в самый низ файла записывается 1 изображение из группы 4. Больше 1 все равно никуда не войдет, поскольку нам нужно обеспечить свободное пространство ниже заявленной высоты фонового изображения. Это часто бывает нужно для плавного перетекания градиента в ровный цвет: градиент вставляется фоновой картинкой, которая заканчивается на фоновом цвете, а дальше продолжается уже этот цвет, заданный через background-color.
  • Производятся абсолютно аналогичные действия с repeat-y.
  • Далее изображения из группы 7 объединяются по вертикали (0 100% означает, что фон должен быть прижат к правому краю элемента, соответственно, весь спрайт будет "прижат" к правому своему краю).
  • Аналогично с группой 8 — прижимаем все к низу. Естественно, что для всех групп мы учитываем первоначальное значение background-position.
  • Рассчитываем позиционирование для изображений группы 1 (для этого подойдет и просто перебор отсортированных по площади или сумме квадратов измерений изображений: готовим матрицу, в которую пытаемся "вписать" очередное изображение; если не получается, то матрицу увеличиваем). При вписывании ради экономии процессорного времени можно проверять только 9 точек (8 по краям изображения и 1 в центре), чтобы убедиться, что на месте изображения еще ничего нет.
  • Строим "лесенку" из изображений второй группы. Лесенку лучше строить с низа уже созданного спрайта из предыдущего пункта: тогда легко найти минимальный размер "дырки" между двумя группами изображений, чтобы сдвинуть "лесенку" вверх (и потом, возможно, влево). Конечно, поиск наиболее оптимального расположения — непростая задача. Но ее можно решить и в достаточно грубом приближении, которое описано выше.
  • Итоговое изображение из пункта 4 прикрепляется к правому верхнему углу нашего сложного изображения (результат действия пунктов 6 и 7). Так как у каждого такого изображения задана допустимая высота и все они находятся вне "рабочей" зоны остальных no-repeat изображений, никаких рудиментов не возникнет.
  • Аналогичным образом поступаем с изображением из пункта 5 — его располагаем в нижнем левом углу нашего спрайта.
  • На выходе мы получаем 3 спрайта со всеми возможными случаями. В среднем размеры таких спрайтов будут лишь незначительно превосходить (если вообще будут) аналогичные "ручные" аналоги (в том числе за счет использования PNG). Естественно, можно в автоматическом режиме пропустить их через pngcrush или jpegtran. Стоит также предусмотреть, в каком виде будут создаваться полноцветные изображения: JPEG или PNG (последние больше по размеру, но гарантируют отсутствие потерь качества).
  • При расчете позиции картинки в конечном спрайте может помочь следующая схема:

    (рис 4.1) Схема позиционирования фоновой картинки относительно элемента

    В силу громоздкости решения (в нем более 1500 строк кода) в полном объеме в данной книге оно не приводится, однако все описанные шаги уже применены в Web Optimizer (http://www.web-optimizer.ru/) (веб-приложении для автоматизации клиентской оптимизации). Одна из финальных версий алгоритма работает для инструмента Auto Sprites (http://sprites.in/), а с исходным текстом можно ознакомиться в SVN (http://code.googLe.eom/p/ web-optimizator/source/browse/trunk/Libs/ php/css.sprites.php).

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

    4.3. CSS Sprites и data:URI, или Microsoft и весь остальной мир

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

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

  • несемантическая верстка в случае использования сложных спрай-тов. Это приводит к дополнительному времени создания документа (особенно существенно может быть для "старых" браузеров). (Более подробно о влиянии DOM-дерева на процесс загрузки страницы рассказывается в следующей лекции);
  • невозможность комбинирования нескольких осей повторения. Это не очень существенно, поскольку все использование CSS Sprites (как было описано в предыдущем разделе) можно свести к трем основным изображениям;
  • тяжесть изменения картинки в случае сложной геометрии;
  • отображение неверного фона при масштабировании.
  • 4.3.1. data:URI

    Есть ли выход из этого положения? Да, есть. Это технология data:URI, которая позволяет включать фоновые изображения прямо в CSS-файл в base64-виде. Плюсы данного подхода очевидны: не нужно "склеивать" несколько картинок в один файл, есть возможность объединять различные оси и анимированные с обычными изображениями, полностью отделить содержание (семантику документа) от его представления (оформления и дизайна), и т. д.

    Но есть и ложка дегтя: IE вплоть до версии 7 не поддерживает data:URI. IE8 — уже да, но все остальные IE — нет. Что делать?

    4.3.2. mhtml

    Нам на помощь приходит технология mhtml (MIME HTML), которую поддерживает по умолчанию только IE (почти в полной мере) и Opera (начиная с версии 9.0). Она позволяет включать base64-данные в CSS-файл в виде комментариев. В этом случае сам файл выступает в двух ипостасях: как таблица стилей и как хранилище фоновых картинок.

    Если мы объединим эту технологию с data:URI, то все будет хорошо. Правда?

    4.3.3. Проблема 1: долгая предзагрузка

    В случае включения фоновых картинок прямо в CSS-файл последний заметно увеличивается в размере (даже при использовании gzip-сжатия). Это значительно увеличивает время предзагрузки (если фоновых картинок больше 10—15 Кб), и пользователь дольше видит белый экран. Опять все плохо. Как быть?

    Возможным выходом из сложившейся ситуации может стать подключение CSS-файла с фоновыми картинками по комбинированному событию window.onload, что вынесет загрузку элементов дизайна в ту область, где она изначально находилась: на стадию полной загрузки страницы или даже в пост-загрузку. В данном случае мы получаем полную аналогию метода CSS Sprites, только без заявленных выше неудобств.

    4.3.4. Проблема 2: выключенный JavaScript

    Описанный выше прием позволит облегчить загрузку только пользователей с включенным (или поддерживаемым) JavaScript (их порядка 9899%). Однако в ряде проектов это может быть недостаточно. Для оставшихся пользователей мы можем через <noscript> подключить нужный нам файл (и конкретно для них замедлить предзагрузку) или поместить вызов этого файла перед </body> (что в ряде случаев может быть аналогично подключению стилей в <head> ).

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

    4.3.5. Проблема 3: Safari и window.onload

    Из-за того, что Safari отличается алгоритмом обновления страницы (что позволяет значительно ускорить отображение самих страниц), для этого браузера не удается загрузить дополнительные стили после отображения первоначальной картинки (на стадии полной загрузки страницы). В этом случае Safari блокирует отрисовку картинки на экран и ожидает загрузку нового файла стилей.

    На данный момент для Safari мы можем безболезненно загружать дополнительные файлы стилей только по полному событию window.onload.

    4.3.6. Проблема 4: Microsoft, IE7 и Windows Vista

    В результате проведенных исследований удалось установить, что в связи с проблемами безопасности в Vista mhtml-технология для отображения фоновых изображений не поддерживается. В этом случае единственным выходом будет подключение конкретно для этого браузера (через JavaScript) общего файла (с использованием CSS Sprites). Для пользователей IE7@Vista с отключенным JavaScript у нас нет другой альтернативы, чем загружать файл только с CSS Sprites.

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

    4.4. Автоматизация кроссбраузерного решения для data:URI

    (рис 4.2) Логотип duris.ru: Data:URI [CSS] Sprites. Источник: duris.ru

    Многим профессиональным веб-разработчикам известны приемы оптимизации сайтов. Одним из способов оптимизации является применение CSS Sprites. Этим же разработчикам известно, какие существуют трудности с формированием, сборкой и пересборкой стандартных спрайтов. При использовании стандартных спрайтов полностью автоматизировать сборку для всех случаев проблематично, из-за специфики свойств background в CSS. Иногда камнем преткновения является свойство repeat фоновой картинки.

    4.4.1. Data:URI CSS

    Существует альтернативный вариант генерации CSS-спрайтов — на основе схемы data:URI. Данный подход интересен тем, что максимально минимизируется количество обращений к серверу, и самое важное — можно полностью автоматизировать процесс сборки и перегенерации спрайтов. Для полной автоматизации процесса оптимизации работы сайтов и сборки CSS-спрайтов на основе data:URI схемы была разработана специальная библиотека.

    Мучения со стандартным подходом применения CSS-спрайтов, а именно, трудности модернизации и, в некоторых случаях, сложности оптимальной компоновки заставили искать альтернативный вариант оптимизации загрузки изображений. Далее речь пойдет об использовании комбинированного метода: data:URI + mhtml. Более детально процесс создания изображений в этом формате описан в пятой главе книги "Разгони свой сайт".

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

    Новый подход генерации CSS-спрайтов на основе data:URI решили назвать Data URI Sprites — DURIS.ru. Название немного необычное — но зато уникальное и хорошо запоминается.

    4.4.2. Что это?

    В первую очередь это полностью автоматический анализ и сборка CSS-спрайтов на основе data:URI.

    Некоторые характеристики работы DURIS:

  • загрузка и анализ всех внутренних ( <style> ) и внешних ( <link> ) стилей;
  • выделение background-image в отдельный внешний стиль;
  • загрузка и кодирование в base64 всех изображений, которые найдены в стилях;
  • оптимизация правил с повторяющимися background-image в стилях;
  • удаление CSS-правил с отсутствующими на сервере изображениями (устраняет лишние ненужные запросы);
  • специальное подключение data:URI спрайтов для всех браузеров и отдельно для IE6, IE7 Vista (более детально в FAQ, duris.ru/faq/).
  • 4.4.3. Зачем это надо?

    Современный подход создания CSS-спрайтов:

  • позволяет безболезненно вносить коррективы в изображения и верстку;
  • дает возможность свести к минимуму число запросов к серверу для получения информации, которая относится к оформлению страницы;
  • позволяет использовать полностью семантическую верстку;
  • устраняет проблемы масштабирования для фоновых изображений;
  • объединяет изображения разных типов и осей повторения;
  • все создается в автоматическом режиме.
  • 4.4.4. IE и другие

    В ходе разработки реализации data:URI спрайтов были проанализированы наиболее часто встречающие варианты CSS-правил. Также был учтен всеми любимый браузер IE. Кому еще не известно: IE не поддерживает до версии 8 технологию data:URI. Однако для него существует альтернативный вариант реализации спрайтов — на основе mhtml-технологии. Другими словами, на данный момент мы имеем полный спектр решений для всех современных браузеров (99% процентов всех используемых браузеров). Но, как всегда, "ложку дегтя" подкидывает Microsoft. Во время тестирования найдена ошибка mhtml-технологии в Vista IE7 — а именно, браузер IE7 в ОС Vista при кэшировании mhtml-файла отказывается показывать изображения (это связано с малодокументированными проблемами безопасности при использовании mhtml в Vista IE7). Если сделать файл некэшируемым, то все работает, как и должно работать, но в случае с кэшированием фоновые изображения не отображаются.

    В общем, Microsoft все же сделал так, чтобы "цепь разорвалась". На текущий момент для браузера IE7 в ОС Vista реализация DURIS работает не совсем так, как задумывалось изначально. В подключение стилей внедрен алгоритм проверки IE7 Vista и в случае обнаружения такового фоновые картинки подключаются стандартным путем. Статистика показывает, что пользователей, которые используют IE7 под Vista, около 5% процентов. В любом случае, мы уже знаем, что в IE8 нормально реализована data^RI-технология.

    4.4.5. Основные достоинства

    Выделим два наиболее важных достоинства использования современного подхода генерации CSS-спрайтов.

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

    На сегодняшний день мы имеем стабильную бета-версию DURIS, которая отрабатывает все передаваемые ей CSS-файлы. Отрабатываются также специфические правила, такие как filter:AlphaImageLoader, !important. Ядро DURIS разработано на языке Java и является самодостаточным (т. е. не зависит от сайта). Предполагается, что после получения релиз-кандидата исходный код ядра будет выложен в открытый доступ под OpenSource-лицензией. Ядро работает с командной строки наподобие того, как работает YUI Compressor. Это позволит удобно интегрировать автоматическую генерацию CSS-спрайтов в свои проекты. Кто программирует на Java, при желании сможет напрямую вызывать методы ядра DURIS.

    4.4.7. Итого

    Разработанный метод/алгоритм автоматической генерации CSS-спрай-тов основе data:URI уникален в своем роде и не имеет мировых аналогов. На сайте выложен FAQ (http://duris.ru/faq/), в котором детально описано, что и как работает. Если что не понятно — задаем вопросы в комментариях.

    В роли главного разработчика данного решения выступил Руслан Си-ницкий (aka sirus, http://fullajax/#:developers).

    4.4.8. Будущее

    При появлении поддержки в IE8 схемы data:URI (стоит все же помнить об ограничении в 32 Кб, http://msdn.microsoft.com/en-us/library/cc848897%28VS.85%29.aspx) разработанный подход становится довольно перспективным, теперь все современные браузеры поддерживают такие спрайты.

    Детально ознакомиться с принципами генерации и подключения data:URI CSS-спрайтов можно в разделе FAQ (http://duris.ru/faq/). Если у вас возникли вопросы или предложения по работе сайта или алгоритма в целом, вы можете оставить свои пожелания на этом сайте в форме обратной связи.

    Стоит также понимать, что наиболее эффективные методы клиентской оптимизации будут использовать комбинированные решения (как, например, это реализовано в Web Optimizer). Часть фоновых изображений, которые сложно или нерационально "склеивать" в CSS Sprites и которые невелики по размеру, мы можем включить прямо в CSS-файл. Размер его при этом увеличится незначительно.

    Большие же изображения (больше 24 Кб) мы не можем включать из принципов кроссбраузерности, они могут формировать CSS Sprites и этим не только уменьшать общее время загрузки, но и формировать наиболее оптимальную визуальную скорость загрузки сайта у конечного пользователя. При этом часть картинок (включенных через комбинированный data:URI) появится сразу же, а часть (включенных через CSS Sprites) — через некоторое время, так как они будут представлены отдельными файлами.

    4.5. Автоматизация кэширования

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

    4.5.1. Кэширование на клиентском уровне

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

    Статические ресурсы без сжатия

    Форсирование кэширования для статических ресурсов может выполняться без сжатия. В данном случае мы ничем не рискуем, выставляя не только максимальное время кэширования, но и предлагая кэшировать ресурсы на локальных прокси-серверах (директива Cache-Control: public ). Для PHP у нас получится следующий код (в Expires прописана дата на 10 лет вперед относительно текущего времени на сервере):

    <?php
    header("Cache-Control: public, max-age=315360000");
    header("Expires: Mon, 01 Jul 2019 00:00:00");
    ?>

    В случае выставления директив для Apache:

    <FilesMatch \.(bmp|png|gif|jpe?g|ico|swf|flv|pdf|tiff)$>
    Header append Cache-Control public
    ExpiresActive On
    ExpiresDefault "access plus 10 years"
    <FilesMatch>

    И в случае nginx:

    location ?* ^.+\.(bmp|gif|jpg|jpeg|png|swf|tiff|swf|flv)$ {
    expires 10y;
    header set Cache-Control public;
    }

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

    Статические ресурсы со сжатием

    Все отличие от предыдущего случая заключается в том, что разрешить локальным прокси-серверам кэшировать такие ресурсы (обычно это CSS-, JavaScript- или ICO-файлы) нельзя. Даже больше: нам нужно запретить кэширование сжатых версий файлов, чтобы избежать выдачи сжатого файла тем пользователям, которые сжатия не поддерживают. Код для PHP:

    <?php
    header("Cache-Control: private, max-age=315360000");
    header("Expires: Mon, 01 Jul 2019 00:00:00");
    ?>

    Для Apache:

    <FilesMatch \.(css|js|ico)$>
    Header append Cache-Control private
    ExpiresActive On
    ExpiresDefault "access plus 10 years"
    </FilesMatch>

    И для nginx:

    location ~* ^.+\.(css|js|ico)$ {
    expires 10y;
    header set Cache-Control private;
    }

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

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

    Запрет кэширования динамических ресурсов

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

    Для того чтобы запретить кэширование во всех браузерах HTML-документов, нужно написать с помощью PHP (в Expires прописано текущее время на сервере):

    header("Expires: Wed, 01 July 2009 00:00:00");
    header("Cache-Control: no-store, no-cache, must-revalidate,
    private");
    header("Pragma: no-cache");

    Для Apache:

    <FilesMatch \.(php|phtml|shtml|html|xml|htm)$>
    ExpiresActive Off
    Header append Cache-Control "no-store, no-cache,
    must-revalidate, private"
    Header append Pragma "no-cache"
    </FilesMatch>

    Для nginx:

    location ?* ^.+\.(php|phtml|shtml|html|xml|htm)$ {
    expires 0;
    header set Cache-Control "no-store, no-cache, must-revalidate,
    private";
    header set Pragma "no-cache";
    }

    4.5.2. Условное кэширование

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

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

    Существует два способа обеспечить условное кэширование: при помощи заголовков Last-Modified + If-Modified-Since и ETag + If-None-Match. Первый заголовок в паре относится к ответу со стороны сервера, второй — к запросу со стороны клиента, который уже получил соответствующий ответ сервера и теперь желает проверить, изменилось ли что-нибудь с той поры.

    При помощи пары Last-Modified мы можем установить соответствие ресурса только по времени его изменения. ETag (англ. Entity Tag, метка сущности) предполагает более широкую сферу применения: мы можем для ресурса назначить уникальную строку, зависящую не только от времени изменения, но и от других параметров (например, передается данный файл в сжатом виде или нет).

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

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

    /* получаем содержимое документа, например, из закэшированного
    файла */
    $content = @file_get_contents($file);
    /* вычисляем уникальную метку, зависящую от содержания */
    $hash = md5($content);
    /* проверяем соответствие вычисленной и переданной с клиента
    меток */
    if ((isset($_SERVER['HTTP_IF_NONE_MATCH']) 
    stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) == '"' . $hash .
    '"') ||
    (isset($_SERVER['HTTP_IF_MATCH']) 
    stripslashes($_SERVER['HTTP_IF_MATCH']) == '"' . $hash . '"')) {
    /* в случае совпадения меток передаем 304-ответ */
    header ("HTTP/1.0 304 Not Modified");
    /* и не передаем содержимое документа */
    header ("Content-Length: 0");
    exit();
    }
    /* во всех остальных случаях выставляем заголовок ETag */
    header("ETag: \"" . $hash . "\"");
    /* и отдаем полностью содержимое документа */
    echo $content;

    4.5.3. Сброс кэша

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

    Строка запроса

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

    Рассмотрим следующий вызов CSS-файла:

    <link rel="stylesheet" href="/css/main.css?20090701" type="text/css"/>

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

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

    <?php
    /* получаем метку времени, равную времени изменения файла */
    $timestamp = filemtime($file);
    /* выводим ссылку на файл в HTML-документе */
    echo '<link rel="stylesheet" href="/css/main.css?". $timestamp .
    "' type="text/css"/>';
    ?>

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

    Физическое имя файла

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

    RewriteRule ~(.*)(\.v[0-9]+)?\.(css|js)$ $1.$2 [QSA,L]

    Какой оно несет смысл? А тот, что при указании в HTML-документе ссылки на файл

    main.layout.v123456.css

    сервер отдаст физический файл

    main.layout.css

    Таким образом мы элегантно обходим проблему прокси-серверов одной строкой в конфигурации сервера. Соответствующий PHP-код будет выглядеть так:

    <?php
    /* получаем метку времени, равную времени изменения файла */
    $timestamp = filemtime($file);
    /* выводим ссылку на файл в HTML-документе */
    echo '<link rel="stylesheet" href="/css/main.css.v". $timestamp .
    "' type="text/css"/>';
    ?>

    Однако, как и в предыдущем случае, мы все равно обращаемся к файловой системе.

    Создание хэша

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

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

    $hash = '';
    foreach ($this->initial_files as $file) {
    $hash .= $file['file_raw'];
    }
    $new_file_name = md5($hash);

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

    Использование разделенной памяти

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

  • подключение библиотек разделяемой памяти (APC, eAccelerator, memcache);
  • возможность управлять состоянием кэша (редактирование проверяемых файлов через веб-интерфейс, частичный сброс кэша либо полный сброс закэшированных файлов). На примере APC описанный алгоритм выглядит следующим образом:
    <?php
    /* удаляем (ставим время истечения равным 1 с) из APC запись
    относительно */
    /* текущего файла, при изменении каких-либо включенных в него
    файлов */
    if ($changed) {
    apc_store($new_file_name, apc_fetch($new_file_name), 1);
    }
    …
    /* при выдаче закэшированного файла проверяем, нужно ли
    его пересобирать */
    $mtime = apc_fetch($new_file_name);
    if (!$mtime) {
    …
    /* если нужно, то при создании файла записываем текущее время
    в APC */
    $mtime = $_SERVER['REQUEST_TIME'];
    echo '<link rel="stylesheet" href="/css/main.css?". $mtime .
    "' type="text/css"/>';
    apc_store($new_file_name, $mtime);
    } else {
    /* если нет, то у нас уже получено время изменения файла,
    которое можно использовать для метки кэша */
    echo '<link rel="stylesheet" href="/css/main.css?". $mtime .
    "' type="text/css"/>';
    }
    ?>
  • Как можно видеть, предложенный алгоритм весьма прост (и может сводиться к простой процедуре очистки кэша, когда мы для всех созданных комбинированных файлов форсируем пересборку), но позволяет полностью избежать обращения к файловой системе (или ее кэшу) при действиях по клиентской оптимизации страницы.

    Таким образом, кэширование на клиентском уровне может ускорить загрузку последующих страниц (или посещений) вашего сайта на 500-1000% (в 5-10 раз), а правильное управление кэшированием гарантирует вам, что информация,получаемая пользователями, всегда будет актуальной.

    4.5.4. Кэширование на серверном уровне

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

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

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

    Отдаем закэшированный документ

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

    Для более детального примера давайте рассмотрим следующий код, отвечающий за простейшее серверное кэширование, из Web Optimizer:

    /* проверяем, можем ли отдать закэшированный документ */
    if (!empty($this->cache_me)) {
    /* переводим адрес документа в реальное имя файла */
    $this->uri = $this->convert_request_uri();
    $file = $this->options['page']['cachedir'] . '/' . $this->uri;
    /* проверяем, существует ли файл и достаточно ли он актуальный */
    if (is_file($file) 
    $_SERVER['REQUEST_TIME'] - filemtime($file) <
    $this->options['page']['cache_timeout']) {
    $content = @file_get_contents($file);
    /* проверяем, сжат ли файл */
    if (!empty($this->options['page']['gzip']) 
    substr($content, 0, 8) ==
    "\x1f\x8b\x08\x00\x00\x00\x00\x00") {
    /* если сжат, то выставляем соответствующие заголовки */
    $this->set_gzip_header();
    }
    /* отдает закэшированное содержимое */
    echo $content;
    /* и закрываем PHP-процес */
    die();
    }
    }

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

    Создаем закэшированный документ

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

    /* определяем, нужно ли нам сохранять закэшированную версию
    документа */
    if (!empty($this->cache_me)) {
    /* формируем имя файла */
    $file = $options['cachedir'] . '/' . $this->uri;
    /* проверяем, есть ли такой файл и не устарел ли он */
    if (!is_file($file) ||
    $_SERVER['REQUEST_TIME'] - filemtime($file) >
    $options['cache_timeout']) {
    /* записываем новое содержимое в файл */
    $fp = @fopen($file, "a");
    if ($fp) {
    /* блокируем файл от конкурентных попыток записи */
    @flock($fp, LOCK_EX);
    /* удаляем содержимое и перемещаемся на начало файла */
    @ftruncate($fp, 0);
    @fseek($fp, 0);
    @fwrite($fp, $this->content);
    @fclose($fp);
    }
    }
    }

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

    4.5.5. Кэширование XHR-запросов

    Данный раздел написан после прочтения заметки Steve Souders "F5 and XHR deep dive", посвященной вопросам кэширования XHR-ресурсов.

    Оказывается, что любые данные, полученные при помощи AJAX, никогда не будут обновлены в IE прежде истечения срока действия кэша, даже если вы форсируете обновление ( Ctrl+F5 ). Единственный путь обновить эти данные — это вручную удалить их из кэша.

    Если вы нажимаете Перезагрузку (F5), IE перезапросит все ресурсы (даже с неистекшим сроком действия кэша), за исключением XHR. Это может вызвать большое недоумение среди разработчиков при тестировании, но меня заинтересовало, какие еще проблемы существуют в этом направлении. Будет ли поведение аналогичным во всех остальных основных браузерах? Что произойдет, если срок давности кэша будет в прошлом или заголовок Expires вообще не будет выставлен? Будет ли какой-либо эффект от добавления Cache-Control max-age (который переписывает заголовок Expires)?

    Проводим тестирование

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

  • Вариант с Expires в прошлом добавляет в заголовки ответа Expires, который содержит дату на 30 дней ранее текущей, и Cache-Control с max-age=0.
  • Вариант без Expires вообще не выставляет никаких заголовков Expires или Cache-Control.
  • Вариант с Expires в будущем добавляет в заголовки ответа Expires, который содержит дату на 30 позже текущей, и Cache-Control с max-age=2592000.
  • Ниже в таблице приведены результаты тестирования этой страницы в основных браузерах. Также там записано, перезапрашивался ли XHR-ре-сурс или был прочитан из кэша, а если перезапрашивался, то с каким кодом HTTP-статуса.

    Ниже приведены соображения на тему того, что происходит при нажатии F5.

  • Все браузеры перезапрашивают и картинку, и внешний скрипт (это имеет смысл).
  • Все браузеры перезапрашивают XHR-ресурс, если срок действия кэша находится в прошлом (это тоже имеет смысл: браузер знает, что закэшированный XHR-ресурс устарел).
  • Если кэшируется XHR, что происходит при нажатии F5?
    Expires в прошлом Без Expires Expires в будущем
    Chrome 2 304 304 304
    Firefox 3.5 304 304 304
    IE 7 304 кэш кэш
    IE 8 304 кэш кэш
    Opera 10 304 кэш 304
    Safari 4 200 200 200
  • Единственное различие в поведении замечено в тот момент, когда для XHR-ресурса нет заголовка Expires, или же Expires выставлен в будущее. IE 78 не перезапрашивают XHR-ресурс, если нет Expires или Expires выставлен в будущее, даже при нажатии Ctrl+F5. Opera 10 не перезапрашивает XHR-ресурс, если нет Expires (эквивалента для Ctrl+F5 в Opera найти не удалось).
  • И Opera 10, и Safari 4 перезапрашивают favicon.ico во всех случаях (это весьма расточительно).
  • Safari 4 не посылает заголовок If-Modified-Since во всех случаях. В результате ответ всегда приходит со статус-кодом 200 и включает запрашиваемый ресурс полностью. Это верно как для XHR-ре-сурса, так и для картинок и внешних скриптов (это выглядит неоптимально и отличается от поведения других браузеров).
  • Выводы

    Ниже резюмированы рекомендации для веб-разработчиков при работе с XHR-ресурсами.

  • Разработчики должны выставлять срок действия кэша для XHR-ресурсов или в прошлом, или в будущем, чтобы предотвратить расхождения в поведении браузеров, когда Expires вообще не выставлен.
  • Если XHR-ресурсы вообще не должны быть закэшированы, разработчикам стоит выставлять дату изменения ресурса в прошлое. Это давняя проблема с различным поведением браузеров при наличии закэшированных копий определенных ресурсов, и касается она не только XHR-запросов. Например, не всегда пользователи будут перезагружать страницу — они могут на нее попасть, просто переходя по ссылкам. В этом случае браузер выдаст им закэ-шированные версии XHR. Для форсирования сброса кэша мы можем выставлять, например, дополнительный GET-параметр, и это будет работать для всех браузеров и всех прокси-серверов. Более подробно вопросы сброса кэша описаны ранее в этом разделе.
  • Если XHR-запросы желательно кэшировать, то разработчики должны назначить срок истечения кэша в будущем. При тестировании в IE 78 разработчикам придется не забывать очищать кэш, чтобы проверить действие Перезагрузки (F5).
  • Вернуться к учебному плану