На тему автоматической "склейки" стилей и скриптов написано уже довольно много статей, но нигде не было описано полное решение, которое учитывало бы "подводные камни", связанные с браузерами и различными способами использования указанных файлов. Ниже стоит рассмотреть то практическое решение, которое реализовано в Web Optimizer и обкатано уже на нескольких тысячах сайтов.
Несмотря на более простой и поистине академический синтаксис, 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-спецификацией. Здесь разумнее всего воспользоваться одним из трех путей.
$data = ? s!\/\*(.*?)\*\/!!g; # удаляем комментарии
$data = ? s!\s+! !g; # сжимаем пробелы
$data = ? s!\} !}\n!g; # добавляем переводы строки
$data = ? s!\n$!!; # удаляем последний перевод
строки
$data = ? s! \{ ! {!g; # удаляем лишние пробелы
внутри скобок
$data = ? s!; \}!}!g; # удаляем лишние пробелы и
синтаксис внутри скобок
class.csstidy.php ) и осуществить минимизацию простыми вызовами:$css = new csstidy(); $css->load_template($root_dir . 'css.template.tpl'); $css->parse($css_code); echo $css->print->formatted();
При этом для максимального сжатия лучше использовать следующий шаблон ( css.template. ):
|{||{|||;|}||}||{||
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-файлов этот момент стоит учитывать.
Для 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
require 'jsmin-1.1.1.php';
echo JSMin::minify(file_get_contents('example.js'));
Стоит также упомянуть, что классический JSMin не поддерживает
Объединение текстовых файлов способно значительно ускорить загрузку вашего сайта, не причиняя вреда качеству разработки (вы можете разрабатывать отдельно и в автоматическом режиме выкладывать на сайт уже готовые версии файлов). Как показывают данные с webo.in, на произвольном сайте в Рунете используется 2,7 файлов стилей (средний размер — 5,5 Кб) и 5 JavaScript-файлов (средний размер — 15 Кб). Простое их объединение позволит выиграть 0,5-1 с при загрузке страницы. А минимизация (вместе с gzip-сжатием, уменьшающим размер на 85%) — еще 60 Кб, что составит 0,6 с при скорости подключения 100 Кб/с.
Как мы видим, совершенно простые действия способны значительно ускорить загрузку вашего сайта.
В книге "Разгони свой сайт" был опубликован довольно подробный обзор технологии CSS
CSS
Кроме заявленных свойств также есть еще несколько (например, background-color), но они к спрайтам имеют посредственное отношение. Однако стоит добавить к нашим объектам, с помощью которых мы будем формировать карты изображений, размеры (в относительных или абсолютных единицах) и отступы (padding). Это позволит точнее разделить изображения по группам и корректно расположить их в финальном комбинированном изображении.
Естественно, рассматривая набор возможных эффектов при использовании фоновых изображений, стоит отметить следующие:
Здесь основную роль играют конечные размеры объекта (разумеется, если изображение не повторяется по всем осям сразу: в таком случае использовать его для CSS
Все описанные примеры можно более четко структурировать по следующим группам:
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 : относительные единицы.Глядя на эту спецификацию, становится в общем понятно, в каком направлении двигаться для автоматизации создания CSS
Далее речь пойдет уже об инструменте Auto
Для начала нам нужно разобрать все дерево 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-правил */
}
}
}
Дальше начинается самое интересное: как нам вышеописанные группы "склеивать"? Для этого используется следующий алгоритм:
repeat-x изображения (группа 3) объединяются все вместе по вертикали. Попутно правится ширина фоновых изображений (приводится к наименьшему общему кратному). В самое начало такого файла добавляются no-repeat изображения, подходящие по ширине (группа 1). Далее в самый низ файла записывается 1 изображение из группы 4. Больше 1 все равно никуда не войдет, поскольку нам нужно обеспечить свободное пространство ниже заявленной высоты фонового изображения. Это часто бывает нужно для плавного перетекания градиента в ровный цвет: градиент вставляется фоновой картинкой, которая заканчивается на фоновом цвете, а дальше продолжается уже этот цвет, заданный через background-color.repeat-y.background-position .no-repeat изображений, никаких рудиментов не возникнет.При расчете позиции картинки в конечном спрайте может помочь следующая схема:
(рис 4.1) Схема позиционирования фоновой картинки относительно элементаВ силу громоздкости решения (в нем более 1500 строк кода) в полном объеме в данной книге оно не приводится, однако все описанные шаги уже применены в Web Optimizer (http://www.web-optimizer.ru/) (веб-приложении для автоматизации клиентской оптимизации). Одна из финальных версий алгоритма работает для инструмента Auto
Эту логику можно применить на любом этапе веб-разработки (как при начальном создании дизайна, так и при пострелизной оптимизации сайта). Библиотека для автоматического создания
На стадии полной загрузки страницы браузер запрашивает картинки и FLash-анимацию, которые заполняют отведенные им места на странице. По мере появления элементов на странице пользователь ощущает, что страница загружается. Обычно окончание этой стадии совпадает для пользователя с окончанием всей загрузки.
Если говорить об ускорении этой стадии, то здесь одной из основных технологий будет именно технология CSS
Есть ли выход из этого положения? Да, есть. Это технология data:URI, которая позволяет включать фоновые изображения прямо в CSS-файл в
Но есть и ложка дегтя: IE вплоть до версии 7 не поддерживает data:URI. IE8 — уже да, но все остальные IE — нет. Что делать?
Нам на помощь приходит технология mhtml (MIME HTML), которую поддерживает по умолчанию только IE (почти в полной мере) и Opera (начиная с версии 9.0). Она позволяет включать
Если мы объединим эту технологию с data:URI, то все будет хорошо. Правда?
В случае включения фоновых картинок прямо в CSS-файл последний заметно увеличивается в размере (даже при использовании gzip-сжатия). Это значительно увеличивает время предзагрузки (если фоновых картинок больше 10—15 Кб), и пользователь дольше видит белый экран. Опять все плохо. Как быть?
Возможным выходом из сложившейся ситуации может стать подключение CSS-файла с фоновыми картинками по комбинированному событию window.onload, что вынесет загрузку элементов дизайна в ту область, где она изначально находилась: на стадию полной загрузки страницы или даже в пост-загрузку. В данном случае мы получаем полную аналогию метода CSS
Описанный выше прием позволит облегчить загрузку только пользователей с включенным (или поддерживаемым) JavaScript (их порядка 9899%). Однако в ряде проектов это может быть недостаточно. Для оставшихся пользователей мы можем через <noscript> подключить нужный нам файл (и конкретно для них замедлить предзагрузку) или поместить вызов этого файла перед </body> (что в ряде случаев может быть аналогично подключению стилей в <head> ).
В качестве еще одного варианта можно рассмотреть создание единственного CSS-файла для таких пользователей, чтобы максимально ускорить им загрузку в случае отключенного JavaScript.
Из-за того, что Safari отличается алгоритмом обновления страницы (что позволяет значительно ускорить отображение самих страниц), для этого браузера не удается загрузить дополнительные стили после отображения первоначальной картинки (на стадии полной загрузки страницы). В этом случае Safari блокирует отрисовку картинки на экран и ожидает загрузку нового файла стилей.
На данный момент для Safari мы можем безболезненно загружать дополнительные файлы стилей только по полному событию window.onload.
В результате проведенных исследований удалось установить, что в связи с проблемами безопасности в Vista mhtml-технология для отображения фоновых изображений не поддерживается. В этом случае единственным выходом будет подключение конкретно для этого браузера (через JavaScript) общего файла (с использованием CSS
Более подробно о создании автоматического решения, позволяющего решить все описанные проблемы, рассказывается в следующем разделе
(рис 4.2) Логотип duris.ru: Data:URI [CSS] Sprites. Источник: duris.ruМногим профессиональным веб-разработчикам известны приемы оптимизации сайтов. Одним из способов оптимизации является применение CSS background в CSS. Иногда камнем преткновения является свойство repeat фоновой картинки.
Существует альтернативный вариант генерации CSS-data:URI. Данный подход интересен тем, что максимально минимизируется количество обращений к серверу, и самое важное — можно полностью автоматизировать процесс сборки и перегенерации data:URI схемы была разработана специальная библиотека.
Мучения со стандартным подходом применения CSS-data:URI + mhtml. Более детально процесс создания изображений в этом формате описан в пятой главе книги "Разгони свой сайт".
В ходе дискуссий и data:URI существует возможность автоматизации процесса. Соответственно, было принято решение создать программное обеспечение для автоматической сборки data:URI
Новый подход генерации CSS-data:URI решили назвать Data URI
В первую очередь это полностью автоматический анализ и сборка CSS-data:URI.
Некоторые характеристики работы DURIS:
<style> ) и внешних ( <link> ) стилей;base64 всех изображений, которые найдены в стилях;background-image в стилях;data:URI Современный подход создания CSS-
В ходе разработки реализации data:URI 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-технология.
Выделим два наиболее важных достоинства использования современного подхода генерации CSS-
На сегодняшний день мы имеем стабильную бета-версию DURIS, которая отрабатывает все передаваемые ей CSS-файлы. Отрабатываются также специфические правила, такие как filter:AlphaImageLoader, !important. Ядро DURIS разработано на языке Java и является самодостаточным (т. е. не зависит от сайта). Предполагается, что после получения релиз-кандидата исходный код ядра будет выложен в открытый доступ под OpenSource-лицензией. Ядро работает с командной строки наподобие того, как работает YUI
Разработанный метод/алгоритм автоматической генерации CSS-спрай-тов основе data:URI уникален в своем роде и не имеет мировых аналогов. На сайте выложен
В роли главного разработчика данного решения выступил Руслан Си-ницкий (
При появлении поддержки в IE8 схемы data:URI (стоит все же помнить об ограничении в 32 Кб, http://msdn.microsoft.com/en-us/library/cc848897%28VS.85%29.aspx) разработанный подход становится довольно перспективным, теперь все современные браузеры поддерживают такие
Детально ознакомиться с принципами генерации и подключения data:URI CSS-
Стоит также понимать, что наиболее эффективные методы клиентской оптимизации будут использовать комбинированные решения (как, например, это реализовано в Web Optimizer). Часть фоновых изображений, которые сложно или нерационально "склеивать" в CSS
Большие же изображения (больше 24 Кб) мы не можем включать из принципов кроссбраузерности, они могут формировать CSS
Кэширование на клиентском и серверном уровнях может значительно ускорить скорость работы сайта. На данный момент все современные системы управления сайтом включают поддержку кэширования, в некоторых случаях даже многоуровневого.
Настройка кэширования для предотвращения дополнительных запросов из браузера к серверу осуществляется достаточно просто: нужно всего лишь знать наиболее характерные случаи использования.
Форсирование кэширования для статических ресурсов может выполняться без сжатия. В данном случае мы ничем не рискуем, выставляя не только максимальное время кэширования, но и предлагая кэшировать ресурсы на локальных прокси-серверах (директива 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";
}
Для динамических ресурсов очень часто оказывается, что обеспечить какой-либо срок кэширования невозможно: содержимое документа постоянно изменяется или полностью зависит от пользовательских действий. Если рассмотреть статические файлы, то и для них бывает весьма полезно настроить проверку изменения ресурса с течением времени. Например, установить срок действия кэша на месяц, а чтобы не увеличивать объем передаваемых данных, через месяц проверять, изменился ли запрашиваемый объект или нет.
В том случае, если объект не изменился, мы можем передать клиенту соответствующий заголовок и не передавать все содержимое объекта (ведь оно уже на клиенте имеется). Такой метод кэширования (когда мы в зависимости от определенных условий либо передаем в браузер полностью содержимое файла, либо отвечаем, что файл не изменился) называется условным кэшированием.
Существует два способа обеспечить условное кэширование: при помощи заголовков 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;
Довольно часто от кэширования на клиентском уровне отказываются в силу того, что трудно бывает корректно контролировать поведение всех браузеров в случае частичного или полного изменения ресурсов, которые находятся в кэше.
На самом деле эта проблема обходится достаточно просто, и в книге "Разгони свой сайт" уже было приведено несколько способов обеспечения этого механизма. Наиболее простой путь заключается в том, что мы можем добавлять специальный 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-документа мы будем дополнительно запрашивать файловую систему на предмет изменения сопутствующих файлов.
Указанное выше решение обладает еще одним небольшим недостатком: некоторые прокси-серверы не будут кэшировать файлы со строкой запроса, считая их динамическими. Мы можем обойти данную ситуацию через
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);
Таким образом мы будем обновлять клиентский кэш каждый раз, когда у нас изменится хотя бы один вызов тех файлов, которые формируют хэш. Это не решает проблемы, затронутой в начале лекции (нам все равно нужно проверять сам файл, в котором объединено несколько, на дату изменения), но помогает решить схожую проблему, которая возникает по ходу обобщения решения на большой проект.
Собственно решение проблемы проверки физических файлов на изменение лежит на поверхности. Для этого нам нужно обеспечить:
<?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 раз), а правильное управление кэшированием гарантирует вам, что информация,получаемая пользователями, всегда будет актуальной.
Довольно часто приходится наблюдать ситуацию, когда время создания страницы на сервере занимает несколько (десятков) секунд. Разбор характерных причин возникновения такого явления и возможные решения мы рассматривать не будем (он выходит за рамки текущей книги), но есть ряд методов для частичного устранения проблем "медленных" страниц. Речь идет о простейшем кэшировании созданных 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);
}
}
}
Правильно настроенное кэширование на серверном уровне способно сэкономить время ваших посетителей (и тем самым поднять конверсию сайта) и сэкономить серверные ресурсы (при использовании каких-либо распределенных мощностей).
Данный раздел написан после прочтения заметки Steve Souders "F5 and XHR deep dive", посвященной вопросам кэширования XHR-ресурсов.
Оказывается, что любые данные, полученные при помощи AJAX, никогда не будут обновлены в IE прежде истечения срока действия кэша, даже если вы форсируете обновление ( Ctrl+F5 ). Единственный путь обновить эти данные — это вручную удалить их из кэша.
Если вы нажимаете Перезагрузку (F5), IE перезапросит все ресурсы (даже с неистекшим сроком действия кэша), за исключением XHR. Это может вызвать большое недоумение среди разработчиков при тестировании, но меня заинтересовало, какие еще проблемы существуют в этом направлении. Будет ли поведение аналогичным во всех остальных основных браузерах? Что произойдет, если срок давности кэша будет в прошлом или заголовок Expires вообще не будет выставлен? Будет ли какой-либо эффект от добавления Cache-Control max-age (который переписывает заголовок Expires)?
Для ответа на все заявленные вопросы была создана тестовая страница. На ней располагалась картинка, внешний скрипт и
Cache-Control с max-age=0.Cache-Control с max-age=2592000.Ниже в таблице приведены результаты тестирования этой страницы в основных браузерах. Также там записано, перезапрашивался ли XHR-ре-сурс или был прочитан из кэша, а если перезапрашивался, то с каким кодом HTTP-статуса.
Ниже приведены соображения на тему того, что происходит при нажатии 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 |
Expires, или же Expires выставлен в будущее. IE 78 не перезапрашивают XHR-ресурс, если нет Expires или Expires выставлен в будущее, даже при нажатии Ctrl+F5. Opera 10 не перезапрашивает XHR-ресурс, если нет Expires (эквивалента для Ctrl+F5 в Opera найти не удалось).If-Modified-Since во всех случаях. В результате ответ всегда приходит со статус-кодом 200 и включает запрашиваемый ресурс полностью. Это верно как для XHR-ре-сурса, так и для картинок и внешних скриптов (это выглядит неоптимально и отличается от поведения других браузеров).Ниже резюмированы рекомендации для веб-разработчиков при работе с XHR-ресурсами.
Expires вообще не выставлен.
На тему автоматической "склейки" стилей и скриптов написано уже довольно много статей, но нигде не было описано полное решение, которое учитывало бы "подводные камни", связанные с браузерами и различными способами использования указанных файлов. Ниже стоит рассмотреть то практическое решение, которое реализовано в Web Optimizer и обкатано уже на нескольких тысячах сайтов.
Несмотря на более простой и поистине академический синтаксис, 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-спецификацией. Здесь разумнее всего воспользоваться одним из трех путей.
$data = ? s!\/\*(.*?)\*\/!!g; # удаляем комментарии
$data = ? s!\s+! !g; # сжимаем пробелы
$data = ? s!\} !}\n!g; # добавляем переводы строки
$data = ? s!\n$!!; # удаляем последний перевод
строки
$data = ? s! \{ ! {!g; # удаляем лишние пробелы
внутри скобок
$data = ? s!; \}!}!g; # удаляем лишние пробелы и
синтаксис внутри скобок
class.csstidy.php ) и осуществить минимизацию простыми вызовами:$css = new csstidy(); $css->load_template($root_dir . 'css.template.tpl'); $css->parse($css_code); echo $css->print->formatted();
При этом для максимального сжатия лучше использовать следующий шаблон ( css.template. ):
|{||{|||;|}||}||{||
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-файлов этот момент стоит учитывать.
Для 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
require 'jsmin-1.1.1.php';
echo JSMin::minify(file_get_contents('example.js'));
Стоит также упомянуть, что классический JSMin не поддерживает
Объединение текстовых файлов способно значительно ускорить загрузку вашего сайта, не причиняя вреда качеству разработки (вы можете разрабатывать отдельно и в автоматическом режиме выкладывать на сайт уже готовые версии файлов). Как показывают данные с webo.in, на произвольном сайте в Рунете используется 2,7 файлов стилей (средний размер — 5,5 Кб) и 5 JavaScript-файлов (средний размер — 15 Кб). Простое их объединение позволит выиграть 0,5-1 с при загрузке страницы. А минимизация (вместе с gzip-сжатием, уменьшающим размер на 85%) — еще 60 Кб, что составит 0,6 с при скорости подключения 100 Кб/с.
Как мы видим, совершенно простые действия способны значительно ускорить загрузку вашего сайта.
В книге "Разгони свой сайт" был опубликован довольно подробный обзор технологии CSS
CSS
Кроме заявленных свойств также есть еще несколько (например, background-color), но они к спрайтам имеют посредственное отношение. Однако стоит добавить к нашим объектам, с помощью которых мы будем формировать карты изображений, размеры (в относительных или абсолютных единицах) и отступы (padding). Это позволит точнее разделить изображения по группам и корректно расположить их в финальном комбинированном изображении.
Естественно, рассматривая набор возможных эффектов при использовании фоновых изображений, стоит отметить следующие:
Здесь основную роль играют конечные размеры объекта (разумеется, если изображение не повторяется по всем осям сразу: в таком случае использовать его для CSS
Все описанные примеры можно более четко структурировать по следующим группам:
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 : относительные единицы.Глядя на эту спецификацию, становится в общем понятно, в каком направлении двигаться для автоматизации создания CSS
Далее речь пойдет уже об инструменте Auto
Для начала нам нужно разобрать все дерево 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-правил */
}
}
}
Дальше начинается самое интересное: как нам вышеописанные группы "склеивать"? Для этого используется следующий алгоритм:
repeat-x изображения (группа 3) объединяются все вместе по вертикали. Попутно правится ширина фоновых изображений (приводится к наименьшему общему кратному). В самое начало такого файла добавляются no-repeat изображения, подходящие по ширине (группа 1). Далее в самый низ файла записывается 1 изображение из группы 4. Больше 1 все равно никуда не войдет, поскольку нам нужно обеспечить свободное пространство ниже заявленной высоты фонового изображения. Это часто бывает нужно для плавного перетекания градиента в ровный цвет: градиент вставляется фоновой картинкой, которая заканчивается на фоновом цвете, а дальше продолжается уже этот цвет, заданный через background-color.repeat-y.background-position .no-repeat изображений, никаких рудиментов не возникнет.При расчете позиции картинки в конечном спрайте может помочь следующая схема:
(рис 4.1) Схема позиционирования фоновой картинки относительно элементаВ силу громоздкости решения (в нем более 1500 строк кода) в полном объеме в данной книге оно не приводится, однако все описанные шаги уже применены в Web Optimizer (http://www.web-optimizer.ru/) (веб-приложении для автоматизации клиентской оптимизации). Одна из финальных версий алгоритма работает для инструмента Auto
Эту логику можно применить на любом этапе веб-разработки (как при начальном создании дизайна, так и при пострелизной оптимизации сайта). Библиотека для автоматического создания
На стадии полной загрузки страницы браузер запрашивает картинки и FLash-анимацию, которые заполняют отведенные им места на странице. По мере появления элементов на странице пользователь ощущает, что страница загружается. Обычно окончание этой стадии совпадает для пользователя с окончанием всей загрузки.
Если говорить об ускорении этой стадии, то здесь одной из основных технологий будет именно технология CSS
Есть ли выход из этого положения? Да, есть. Это технология data:URI, которая позволяет включать фоновые изображения прямо в CSS-файл в
Но есть и ложка дегтя: IE вплоть до версии 7 не поддерживает data:URI. IE8 — уже да, но все остальные IE — нет. Что делать?
Нам на помощь приходит технология mhtml (MIME HTML), которую поддерживает по умолчанию только IE (почти в полной мере) и Opera (начиная с версии 9.0). Она позволяет включать
Если мы объединим эту технологию с data:URI, то все будет хорошо. Правда?
В случае включения фоновых картинок прямо в CSS-файл последний заметно увеличивается в размере (даже при использовании gzip-сжатия). Это значительно увеличивает время предзагрузки (если фоновых картинок больше 10—15 Кб), и пользователь дольше видит белый экран. Опять все плохо. Как быть?
Возможным выходом из сложившейся ситуации может стать подключение CSS-файла с фоновыми картинками по комбинированному событию window.onload, что вынесет загрузку элементов дизайна в ту область, где она изначально находилась: на стадию полной загрузки страницы или даже в пост-загрузку. В данном случае мы получаем полную аналогию метода CSS
Описанный выше прием позволит облегчить загрузку только пользователей с включенным (или поддерживаемым) JavaScript (их порядка 9899%). Однако в ряде проектов это может быть недостаточно. Для оставшихся пользователей мы можем через <noscript> подключить нужный нам файл (и конкретно для них замедлить предзагрузку) или поместить вызов этого файла перед </body> (что в ряде случаев может быть аналогично подключению стилей в <head> ).
В качестве еще одного варианта можно рассмотреть создание единственного CSS-файла для таких пользователей, чтобы максимально ускорить им загрузку в случае отключенного JavaScript.
Из-за того, что Safari отличается алгоритмом обновления страницы (что позволяет значительно ускорить отображение самих страниц), для этого браузера не удается загрузить дополнительные стили после отображения первоначальной картинки (на стадии полной загрузки страницы). В этом случае Safari блокирует отрисовку картинки на экран и ожидает загрузку нового файла стилей.
На данный момент для Safari мы можем безболезненно загружать дополнительные файлы стилей только по полному событию window.onload.
В результате проведенных исследований удалось установить, что в связи с проблемами безопасности в Vista mhtml-технология для отображения фоновых изображений не поддерживается. В этом случае единственным выходом будет подключение конкретно для этого браузера (через JavaScript) общего файла (с использованием CSS
Более подробно о создании автоматического решения, позволяющего решить все описанные проблемы, рассказывается в следующем разделе
(рис 4.2) Логотип duris.ru: Data:URI [CSS] Sprites. Источник: duris.ruМногим профессиональным веб-разработчикам известны приемы оптимизации сайтов. Одним из способов оптимизации является применение CSS background в CSS. Иногда камнем преткновения является свойство repeat фоновой картинки.
Существует альтернативный вариант генерации CSS-data:URI. Данный подход интересен тем, что максимально минимизируется количество обращений к серверу, и самое важное — можно полностью автоматизировать процесс сборки и перегенерации data:URI схемы была разработана специальная библиотека.
Мучения со стандартным подходом применения CSS-data:URI + mhtml. Более детально процесс создания изображений в этом формате описан в пятой главе книги "Разгони свой сайт".
В ходе дискуссий и data:URI существует возможность автоматизации процесса. Соответственно, было принято решение создать программное обеспечение для автоматической сборки data:URI
Новый подход генерации CSS-data:URI решили назвать Data URI
В первую очередь это полностью автоматический анализ и сборка CSS-data:URI.
Некоторые характеристики работы DURIS:
<style> ) и внешних ( <link> ) стилей;base64 всех изображений, которые найдены в стилях;background-image в стилях;data:URI Современный подход создания CSS-
В ходе разработки реализации data:URI 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-технология.
Выделим два наиболее важных достоинства использования современного подхода генерации CSS-
На сегодняшний день мы имеем стабильную бета-версию DURIS, которая отрабатывает все передаваемые ей CSS-файлы. Отрабатываются также специфические правила, такие как filter:AlphaImageLoader, !important. Ядро DURIS разработано на языке Java и является самодостаточным (т. е. не зависит от сайта). Предполагается, что после получения релиз-кандидата исходный код ядра будет выложен в открытый доступ под OpenSource-лицензией. Ядро работает с командной строки наподобие того, как работает YUI
Разработанный метод/алгоритм автоматической генерации CSS-спрай-тов основе data:URI уникален в своем роде и не имеет мировых аналогов. На сайте выложен
В роли главного разработчика данного решения выступил Руслан Си-ницкий (
При появлении поддержки в IE8 схемы data:URI (стоит все же помнить об ограничении в 32 Кб, http://msdn.microsoft.com/en-us/library/cc848897%28VS.85%29.aspx) разработанный подход становится довольно перспективным, теперь все современные браузеры поддерживают такие
Детально ознакомиться с принципами генерации и подключения data:URI CSS-
Стоит также понимать, что наиболее эффективные методы клиентской оптимизации будут использовать комбинированные решения (как, например, это реализовано в Web Optimizer). Часть фоновых изображений, которые сложно или нерационально "склеивать" в CSS
Большие же изображения (больше 24 Кб) мы не можем включать из принципов кроссбраузерности, они могут формировать CSS
Кэширование на клиентском и серверном уровнях может значительно ускорить скорость работы сайта. На данный момент все современные системы управления сайтом включают поддержку кэширования, в некоторых случаях даже многоуровневого.
Настройка кэширования для предотвращения дополнительных запросов из браузера к серверу осуществляется достаточно просто: нужно всего лишь знать наиболее характерные случаи использования.
Форсирование кэширования для статических ресурсов может выполняться без сжатия. В данном случае мы ничем не рискуем, выставляя не только максимальное время кэширования, но и предлагая кэшировать ресурсы на локальных прокси-серверах (директива 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";
}
Для динамических ресурсов очень часто оказывается, что обеспечить какой-либо срок кэширования невозможно: содержимое документа постоянно изменяется или полностью зависит от пользовательских действий. Если рассмотреть статические файлы, то и для них бывает весьма полезно настроить проверку изменения ресурса с течением времени. Например, установить срок действия кэша на месяц, а чтобы не увеличивать объем передаваемых данных, через месяц проверять, изменился ли запрашиваемый объект или нет.
В том случае, если объект не изменился, мы можем передать клиенту соответствующий заголовок и не передавать все содержимое объекта (ведь оно уже на клиенте имеется). Такой метод кэширования (когда мы в зависимости от определенных условий либо передаем в браузер полностью содержимое файла, либо отвечаем, что файл не изменился) называется условным кэшированием.
Существует два способа обеспечить условное кэширование: при помощи заголовков 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;
Довольно часто от кэширования на клиентском уровне отказываются в силу того, что трудно бывает корректно контролировать поведение всех браузеров в случае частичного или полного изменения ресурсов, которые находятся в кэше.
На самом деле эта проблема обходится достаточно просто, и в книге "Разгони свой сайт" уже было приведено несколько способов обеспечения этого механизма. Наиболее простой путь заключается в том, что мы можем добавлять специальный 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-документа мы будем дополнительно запрашивать файловую систему на предмет изменения сопутствующих файлов.
Указанное выше решение обладает еще одним небольшим недостатком: некоторые прокси-серверы не будут кэшировать файлы со строкой запроса, считая их динамическими. Мы можем обойти данную ситуацию через
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);
Таким образом мы будем обновлять клиентский кэш каждый раз, когда у нас изменится хотя бы один вызов тех файлов, которые формируют хэш. Это не решает проблемы, затронутой в начале лекции (нам все равно нужно проверять сам файл, в котором объединено несколько, на дату изменения), но помогает решить схожую проблему, которая возникает по ходу обобщения решения на большой проект.
Собственно решение проблемы проверки физических файлов на изменение лежит на поверхности. Для этого нам нужно обеспечить:
<?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 раз), а правильное управление кэшированием гарантирует вам, что информация,получаемая пользователями, всегда будет актуальной.
Довольно часто приходится наблюдать ситуацию, когда время создания страницы на сервере занимает несколько (десятков) секунд. Разбор характерных причин возникновения такого явления и возможные решения мы рассматривать не будем (он выходит за рамки текущей книги), но есть ряд методов для частичного устранения проблем "медленных" страниц. Речь идет о простейшем кэшировании созданных 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);
}
}
}
Правильно настроенное кэширование на серверном уровне способно сэкономить время ваших посетителей (и тем самым поднять конверсию сайта) и сэкономить серверные ресурсы (при использовании каких-либо распределенных мощностей).
Данный раздел написан после прочтения заметки Steve Souders "F5 and XHR deep dive", посвященной вопросам кэширования XHR-ресурсов.
Оказывается, что любые данные, полученные при помощи AJAX, никогда не будут обновлены в IE прежде истечения срока действия кэша, даже если вы форсируете обновление ( Ctrl+F5 ). Единственный путь обновить эти данные — это вручную удалить их из кэша.
Если вы нажимаете Перезагрузку (F5), IE перезапросит все ресурсы (даже с неистекшим сроком действия кэша), за исключением XHR. Это может вызвать большое недоумение среди разработчиков при тестировании, но меня заинтересовало, какие еще проблемы существуют в этом направлении. Будет ли поведение аналогичным во всех остальных основных браузерах? Что произойдет, если срок давности кэша будет в прошлом или заголовок Expires вообще не будет выставлен? Будет ли какой-либо эффект от добавления Cache-Control max-age (который переписывает заголовок Expires)?
Для ответа на все заявленные вопросы была создана тестовая страница. На ней располагалась картинка, внешний скрипт и
Cache-Control с max-age=0.Cache-Control с max-age=2592000.Ниже в таблице приведены результаты тестирования этой страницы в основных браузерах. Также там записано, перезапрашивался ли XHR-ре-сурс или был прочитан из кэша, а если перезапрашивался, то с каким кодом HTTP-статуса.
Ниже приведены соображения на тему того, что происходит при нажатии 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 |
Expires, или же Expires выставлен в будущее. IE 78 не перезапрашивают XHR-ресурс, если нет Expires или Expires выставлен в будущее, даже при нажатии Ctrl+F5. Opera 10 не перезапрашивает XHR-ресурс, если нет Expires (эквивалента для Ctrl+F5 в Opera найти не удалось).If-Modified-Since во всех случаях. В результате ответ всегда приходит со статус-кодом 200 и включает запрашиваемый ресурс полностью. Это верно как для XHR-ре-сурса, так и для картинок и внешних скриптов (это выглядит неоптимально и отличается от поведения других браузеров).Ниже резюмированы рекомендации для веб-разработчиков при работе с XHR-ресурсами.
Expires вообще не выставлен.Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.