Число запросов является наиболее узким местом при загрузке страницы. По последним исследованиям, порядка 40% времени загрузки уходит только на установление новых соединений с сервером. В этом свете любые методы, позволяющие уменьшить число запросов, выглядят весьма перспективно. Однако каждый такой метод, начиная с простого объединения стилей или скриптов и заканчивая data:URI, достаточно сложен в технологическом плане, поэтому в ряде случаев может просто не окупать затраченного времени.
Зачастую cookie выставляются на весь домен или даже на все поддомены, что означает их отправку браузером даже при запросе каждой картинки с вашего домена. В результате 400-байтный ответ с картинкой превратится в 1000 байтов или даже больше, в зависимости от добавленных заголовков cookie. Если на странице у вас много некэшируемых объектов и большие cookie на домен, то стоит рассмотреть возможность вынесения статичных ресурсов на другой домен (например, так поступил Яндекс, расположив статические файлы на домене yandex.net) и убедиться, что cookie там никогда не появятся.
В силу накладных расходов на передачу каждого объекта один большой файл загрузится быстрее, чем два более мелких, каждый в два раза меньше первого. Стоит потратить время на то, чтобы привести все вызываемые JavaScript-файлы к одному или двум, равно как и CSS-файлы. Если на вашем сайте их используется больше, попробуйте сделать специальные скрипты для публикации файлов на «боевом» сервере или уменьшите их количество. Если на странице в большом объеме располагаются десятки небольших GIF-файлов (для оформления границ или фона элементов), стоит рассмотреть ее преобразование в более простой CSS-дизайн (который не потребует такого большого числа картинок) и(ли) объединение в несколько больших ресурсных файлов.
Для объединения HTML-файлов существует достаточно простое правило по сведению числа фреймов на странице к минимуму (в идеале их вообще не должно быть, ибо каждый фрейм влечет создание нового документа в дереве страницы, что достаточно ресурсоемко). Поэтому давайте рассмотрим, что можно сделать с файлами стилей.
При заботе о производительности веб-страниц мы всегда хотим, чтобы страницы могли быть отрисованы постепенно, чтобы браузер мог отобразить любой контент сразу же, как он у него появится. Это особенно важно для страниц, на которых много текстового содержания, и для пользователей с медленным подключением. Важность визуального оповещения пользователя о текущем состоянии загрузки страницы каким-нибудь индикатором прогресса детально изучена и задокументирована. Однако в любом случае всегда лучше, если в роли индикатора прогресса выступает сама страница. Когда браузер загружает HTML-файл постепенно — сначала заголовок, потом навигацию, логотип наверху и т. д. — все это служит отличным индикатором загрузки для пользователя. Также это улучшает общее впечатление от сайта.
Размещение CSS в конце страницы не позволяет начать постепенное отображение многим браузерам, в числе которых находится и Internet Explorer. Браузер не начинает визуализировать страницу, чтобы не пришлось перерисовывать элементы, у которых во время загрузки изменится стиль. Firefox начинает сразу отрисовывать страницу, в процессе загрузки, возможно, перерисовывая некоторые элементы по мере изменения их свойств, но это является причиной появления нестилизованного контента и рекурсивного его обновления.
Спецификация HTML 4 устанавливает, что таблицы стилей должны быть включены в head документа: «В отличие от , может появляться только в секции , зато там он может встречаться сколько угодно раз». Ни одна из альтернатив — белый экран или показ нестилизованного контента — не стоит этого риска (хотя разработчики Firefox и Opera думают несколько иначе). Оптимальным решением является следование спецификации и включение CSS в head-секцию документа.
При проектировании небольших сайтов либо несложных дизайнов это правило является основным для максимизации производительности. Однако далее в этой главе мы рассмотрим и альтернативную его трактовку.
Зачастую на странице подключается несколько файлов стилей: это может быть связано как с модульной структурой построения CSS, так и с поддержкой различных устройств просмотра веб-страниц. Давайте рассмотрим последний случай: у нас есть два вызова CSS-файлов на странице, например:
где первый используется для отображения страницы на экране монитора, а второй — для предварительного просмотра и печати.
Проблема в том, что браузер не отображает любую часть страницы (это не касается Opera: у нее время отображения страницы без полной загрузке файлов стилей задано по умолчанию в настройках, посмотреть их можно следующим образом: 'preferences' (ctrl-f12) -> 'advanced' -> 'browsing' -> 'loading' или 'инструменты' -> 'настройки' -> 'дополнительно' -> 'перемещение' -> 'загрузка'), пока не загрузит все файлы стилей — в том числе и те из них, которые не предназначены для устройства, с помощью которого производится отображение страницы. Другими словами, браузер не покажет страницу, пока не загрузит и файл стилей для принтера, хотя он совсем и не требуется для визуализации страницы. Это неправильно с точки зрения производительности, но это так (Safari, на самом деле, ведет себя как раз «правильно»: ненужные файлы не задерживают загрузку, но это связано с особенностью модели визуализации; об этом более подробно рассказывается ниже).
Решение выглядит весьма тривиально: мы можем в общем CSS-файле объявить правила для любого устройства через @media. Например, все стили для принтера могут быть записаны в следующем виде:
@media print {
стилевые правила для принтера
}
в конце основного файла стилей. Таким образом, будет загружаться всегда только один файл. Данное решение может быть легко автоматизировано, и некоторые CMS уже применяют этот подход (в частности, Drupal).
Если у нас CSS-файлы разбиты на модули, то нужно пересмотреть их структуру таким образом, чтобы на каждую страницу приходилось не более двух файлов (небольшие файлы — порядка 5 Кб — можно объединить в один для целого раздела). Для главной страницы я рекомендую всегда ограничиться только одним файлом либо вообще включать его в HTML-код (как сделано, например, для главной страницы Яндекса).
Очень часто верстка страниц производится таким образом, что у нас появляется основной файл стилей и несколько дополнительных, рассчитанных на конкретные браузеры (речь идет в основном об Internet Explorer, однако иногда требуются какие-то специальные правила для Firefox, Opera или Safari). В этом случае файлы подключают через так называемые «условные комментарии», которые выглядят как обычные HTML-комментарии для всех браузеров, кроме Internet Explorer (у остальных браузеров есть свои способы загрузить какой-то файл стилей только для них).
Финальная конструкция выглядит примерно следующим образом:
href="ie6.css" media="screen" />
Для всех браузеров используется main.css, а для IE6 и ниже — ie6.css. Однако Internet Explorer этих версий не запрашивает файлы стилей параллельно, поэтому при загрузке страницы произойдет ненужная задержка, связанная с доставкой еще одного файла.
Чтобы избежать ее (особенно в случае небольшого количества стилей конкретно для IE), можно использовать CSS-хаки уже в исходном CSS-файле. Например, если нам нужно определить правило только для IE7, мы можем написать так:
*+html body {
margin: 0 auto;
}
для IE6:
* html body {
margin: 0 auto;
}
и для IE5.5- (эта группа браузеров не распознает экранирование символов, поэтому сможет применить только первое правило из двух, второе правило отработает для IE6, переопределив первое):
* html body {
margin: 0;
marg\in: 0 auto;
}
CSS-хаки позволяют совершенно свободно использовать всего один файл стилей для гарантии кроссбраузерности верстки. При этом производительность страницы будет максимальной (исключая, конечно, случай включения всех CSS-правил в исходный HTML-документ — это будет работать еще быстрее, однако чревато некоторыми сложностями; подробнее о них в конце этой главы).
— dom.js
— array.map.js
— array.js
— sprinf.js
— calendar.js
— date.js
— mycoolcombobox.js
— dom.js
— array.map.js
— array.js
— sprinf.js
— animated.pane.js
— pane.js
— dom.js
— array.map.js
— array.js
— sprinf.js
— animation.js
— transition.js
... и так далее ...
— array.js
— array.map.js
— sprinf.js
— dom.js
— array.js
— array.map.js
— sprinf.js
— dom.js
— pane.js
— transition.js
— animation.js
— animated.pane.js
— array.js
— array.map.js
— sprinf.js
— dom.js
— pane.js
— transition.js
— animation.js
— animated.pane.js
// #REQUIRE: array.map.js
// #REQUIRE: sprintf.js
....
код
время отсылки запроса на сервер T1 — для большинства запросов величина практически постоянная;
время формирования ответа сервера — для статических ресурсов, которые мы сейчас и рассматриваем, пренебрежимо мало;
время получения ответа сервера T2, которое, в свою очередь, состоит из постоянной для сервера сетевой задержки L и времени получения ответа R, прямо пропорционального размеру ресурса.
— запросили страницу
— получили HTML
— запросили ресурс A: T1
— получили ресурс A: L + R(A)
— запросили ресурс B: T1
— получили ресурс B: L + R(B)
— запросили ресурс C: T1
— получили ресурс C: L + R(C)
— запросили страницу
— получили HTML
— запросили ресурс A+B+C: T1
— получили ресурс A+B+C: L + R(A + B + C)
P1 — тратим время на загрузку A, B и C
P2 — тратим время на загрузку только D
P3 — тратим время на загрузку E и F
P1 — тратим время на загрузку (A+B+C+D+E+F)
P2 — внешние ресурсы не требуются
P3 — внешние ресурсы не требуются
P1 — тратим время на загрузку (A+B+C)
P2 — тратим время на загрузку (A+C+D)
P3 — тратим время на загрузку (A+С+E+F)
P1 — тратим время на загрузку (A + B) и C
P2 — тратим время на загрузку D
P3 — тратим время на загрузку (E + F)
После разделения JavaScript- и CSS-кода по файлам для поддержания модульной структуры можно в контроллере создать список файлов, которые надо присоединить к данному документу (вместо того чтобы прописывать это вручную в шаблоне отображения). Но теперь надо сделать так, чтобы до показа шаблона вызывалась функция кэширования, которая проходилась бы по списку, проверяла из него локальные файлы на время изменения, объединяла в один файл и создавала или перезаписывала gz-файл с именем, сформированным из md5-хэша имен входящих файлов.
В качестве рабочего примера можно привести следующую функцию:
function cache_js(){
$arrNewJS=array();
$strHash='';
$strGzipContent='';
$intLastModified=0;
// проходимся по списку файлов
foreach ((array)$this->scripts as $file){
if (substr($file,0,5)=='http:') continue;
if ($file[0]=='/') $strFilename=sys_root.$file;
else $strFilename=sys_root.'app/front/view/'.$file;
$strHash.=$file;
// читаем содержимое в одну строку
$strGzipContent.=file_get_contents($strFilename);
$intLastModified=$intLastModified filemtime($strFilename) : $intLastModified; } $strGzipHash=md5($strHash); $strGzipFile=sys_root.'app/front/view/js/bin/'.$strGzipHash.'.gz'; // проверяем, надо ли перезаписать gz-файл if (file_exists($strGzipFile) && $intLastModified>filemtime($strGzipFile) || !file_exists($strGzipFile)){ if (!file_exists($strGzipFile)) touch($strGzipFile); // используем функции встроенной в php библиотеки zlib для архивации $gz = gzopen($strGzipFile,'w9'); gzputs ($gz, $strGzipContent); gzclose($gz); } // перезаписываем список на один файл $arrNewJS[]='js/bin/'.$strGzipHash.'.gz'; $this->scripts=$arrNewJS; } Для CSS основные теоретические моменты описаны выше, а реализация даже несколько проще. Если использовать YUI Compressor, то решение будет совершенно одинаково (вычислили зависимости, склеили файлы, сжали, переименовали, сделали архив) для обоих типов файлов.
К сожалению, почти все описанные выше методы применимы только на стадии разработки или существенной оптимизации и требуют участия опытных разработчиков для своей интеграции. Но возникает резонный вопрос: может быть, уже существуют какие-либо автоматизированные решения для автоматического объединения CSS- или JavaScript-файлов? И что делать, если хочется ускорить сайт на существующей платформе одной из популярных CMS, где в коде уже «сам черт ногу сломит»?
В таких случаях можно использовать проект с открытым кодом PHP Speedy ( http://aciddrop.com/php-speedy/ ) — РНР-скрипт, который обеспечивает расширенное кэширование и сжатие компонентов страницы, не требуя никаких модификаций вручную. Достаточно только установить скрипт на сервере и сделать несложные настройки, заключающиеся в указании директории с самим сайтом и папок для кэширования файлов. Скрипт умеет автоматически склеивать все CSS- и JavaScript-файлы, кэшировать их, применяет оптимизацию (с помощью пакета Minify, http://code.google.com/p/minify/ , о котором уже шла речь выше), а также gzip-сжатие. На выходе мы получаем автоматическую оптимизацию сайта совершенно бесплатно. Хотя, конечно, не следует рассматривать это как конец — для начала такое решение вполне приемлемо, однако, для достижения максимальной производительности сайта придется со временем все больше и больше настраивать некоторые моменты вручную и применять советы из этой книги.
Для владельцев блогов на популярном движке Wordpress есть специальное расширение на основе этого решения, добавляющее оптимизацию буквально одним кликом в панели управления. Решение очень даже полезное: блоги часто расположены на слабых хостинговых площадках, а популярность проекта может возрасти буквально после пары публикаций. Площадка может быть к этому просто не готова, да и сам Wordpress не является вершиной оптимизации с точки зрения нагрузки.
Рассмотрев все аспекты объединения текстовых файлов, перейдем к графической и мультимедийной информации. Сейчас уже много где написано и упомянуто про технику CSS Sprites (или CSS Image Maps). Ниже приведены несколько примеров и полезных ссылок. И пара советов, где и как этот метод может быть применим наиболее оптимальным образом.
Сама техника заключается в том, что мы создаем комбинированное изображение, из которого затем «вырезаем» с помощью CSS-свойства background-position нужный нам в данном случае кусок. На текущем уровне поддержки браузерами (порядка 99,9%) она является просто обязательной для любого уважающего себя веб-ресурса, так как позволяет сократить число запросов к серверу — а кроме этого отделить поведение от представления, и возложить труд по анимации на CSS-движок браузера, а не на JavaScript-движок (т. е. это будет работать даже с выключенными скриптами), и много-много прочих «вкусностей». Но обо всем по порядку.
Обычно таким термином называют смену графической картинки при наведении на нее мыши — своеобразный призыв к действию (как его любят называть маркетологи). У веб-мастеров сложилась дурная практика делать такие эффекты через onmouseover/onmouseout на картинках.
Это прямое нарушения принципа разделение представления от поведения и несемантическая (в лучшем случае, в худшем — еще и невалидная) верстка. И вообще, это очень плохо. В данном случае это делается средствами CSS и является семантически правильным (в большинстве HTML-документов — это ссылка, но с элементами форм приходится немного повозиться, однако тоже ничего сверхъестественного).
Пример: при наведении просто показывается другая картинка.
Соответствующая часть в CSS-коде будет выглядеть примерно так:
a.sprited {
background: yellow url(http://site.ru/img/button.png) 0 0 no-repeat;
width: 100px;
height: 20px;
}
a.sprited:hover {
background-position: -100px 0;
background-color: red;
}
При наведении на ссылку фоновое изображение под ней «уедет», и покажется правая его часть (ибо она изначально скрыта жесткими размерами самой кнопки). Дополнительно мы обеспечили обратную совместимость для тех пользователей, у которых отключены картинки. Для них мы меняем цвет фона при наведении (если картинки в браузере включены, то изображение покажется поверх цвета).
Под таким термином стоит понимать те случаи, когда в одном файле содержится несколько «динамических кнопок». Например, это может быть такое изображение:
В некоторых версиях IE изменение расположения фона при наведении мыши отрабатывает не совсем корректно, и браузер запрашивает исходную фоновую картинку с сервера еще раз, что вызывает «мигание» картинки. Одним из вариантов борьбы с такой проблемой может стать изменение не позиции фонового элемента, а его прозрачности. Например, мы можем сразу задать параметры фона для элемента и его прямого потомка, а при наведении мыши менять фон дочернего элемента на прозрачный, тогда фон родителя будет просто просвечивать:
a {
background: yellow url(http://site.ru/img/button.png) 0 0 no-repeat;
display: block;
height: 20px;
width: 100px;
}
a span {
background: red url(http://site.ru/img/button.png) -100px 0 no-repeat;
display: block;
height: 20px;
width: 100px;
}
a:hover span {
background: transparent;
}
К сожалению, этот метод предполагает появление у элемента несемантического потомка для обеспечения графических эффектов. Более стандартным вариантом будет вызов специфичного для IE метода backgroundImageCache (через try или любое другое условие, гарантирующее обратную совместимость с остальными браузерами):
try {
document.execCommand("BackgroundImageCache", false, true);
} catch (e) {}
В данном случае мы форсируем кэширование фоновых изображений, что предотвращает описанную выше ошибку.
Этот пункт стоит намеренно выделить, ибо он подразумевает более свободное использование ресурсного файла для «подсветки» какого-либо изображения при наведении. Если в предыдущих случаях области были одинакового размера, то тут уже размер областей может быть, вообще говоря, произвольным. Одним из преимуществ такого подхода является совмещение разных областей, чтобы они занимали минимум места. Эта техника как раз и заменила классический Image Map.
Кроме динамических эффектов CSS Sprites широко используется и для объединения статических изображений. Давайте рассмотрим различные плюсы и минусы этого подхода.
Основной опасностью склеивания большого количества иконок в одном месте являются артефакты при увеличении шрифта: посторонняя часть фонового изображения проявляется совершенно не в том месте, где его ожидали, и у пользователя возникает ощущение, что страница «разваливается». Блок становится выше или длиннее, запаса полей данного изображения уже не хватает, в результате у одного элемента отображается сразу несколько иконок. Непорядок. Как с ним бороться, будет рассказано немного ниже в общих советах по созданию ресурсных картинок для CSS Sprites.
В общем случае нужно жестко ограничивать размеры контейнера, у которого заданы определенные фоновые картинки, чтобы даже при увеличенном тексте картинка не «ломалась» (однако текст может стать нечитаемым из-за обрезания по границе блока). Как бороться с последней напастью, также будет рассказано в конце раздела.
В некоторых случаях возможно также объединение всех фоновых изображений на странице в одно-единственное. Такой подход значительно сокращает число запросов к серверу, однако влечет за собой и технологические сложности. Как пример можно привести следующее изображение:
www.csssprites.com. Обладает довольно минималистичным дизайном, есть возможность загружать несколько исходных файлов.
www.printf.ru/spritr/. В этом инструменте есть возможность загружать несколько файлов, очень милый дизайн, но в целом настроек мало.
spritegen.website-performance.org. Тут очень много настроек, также можно гибко создавать и сам CSS-фрагмент, но все картинки нужно загружать одним архивом.
Итак, самое вкусное. Для начала стоит разбить все фоновые картинки на 5 групп (однако пятая группа может быть объединена с любой из первых четырех).
Анимированные картинки
Те, которые предполагается повторять по всем направлениям (repeat)
Те, которые предполагается повторять по горизонтали (repeat-x)
Те, которые предполагается повторять по вертикали (repeat-y)
И те, которые предполагается показывать только один раз (no-repeat)
Откуда взялось разделение на такие группы? Из очень простых соображений: если картинка будет повторяться по какому-то направлению, то по этому направлению она должна быть одна-единственная в своем «окне», иначе повторяться будет не только она одна. Также стоит ориентироваться на общий размер файла в 10–20 Кб: если файл получается больше, то лучше подключать больше одного (соображения по разбиению файлов по размеру более подробно приведены в пятой главе).
Далее, все картинки из второй группы (повторяющиеся по всем направлениям) оставляем как есть (вообще говоря, можно подумать над их преобразованием в стилевые правила для самых простых случаев, но это уже тема для отдельной дискуссии). Все картинки из третьей группы можно склеить по вертикали (тогда в своем горизонтальном окне они будут единственными), все картинки из четвертой группы — по горизонтали. Что же делать с пятой группой?
Тут нам нужно понять, для чего будет использоваться каждая картинка. Если она будет изображать фиксированную по размерам кнопку, то ее можно размещать в любом месте итогового ресурсного файла. Если она будет использована как иконка для списка (размещение в левом верхнем углу элемента), то мы должны очистить все пространство правее и ниже ее. Таким образом, при любом увеличении такого элемента (а «растут» элементы у нас всегда вниз и вправо) ничего лишнего не выводилось. В таком случае иконки располагаются не вертикально, а «лесенкой» (из правого верхнего угла в левый нижний).
Описанная выше проблема с изменением размера надписей в фиксированных кнопках (фон у них фиксированный, поэтому мы не можем их раздвигать при увеличении шрифта, и он обрезается) может быть преодолена путем разбиения фона на 4 части (угловые) и задания соответствующего цвета фона для всех элементов. Однако это повлечет наличие как минимум 4-х вложенных элементов для отображения каждого угла. Не во всех случаях это допустимо семантически. Да, можно создавать дополнительную разметку при помощи JavaScript, но насколько оно того будет стоить? Это лучше решать в каждом конкретном случае.
Давайте теперь перейдем к методам, позволяющим вставлять графическую информацию непосредственно в текстовые файлы без необходимости запроса внешних ресурсов.
Встроенные изображения используют схему data:URI для внедрения прямо в тело веб-страницы. Как было определено в RFC 2397, такие URI предназначены для вставки небольших объектов как «непосредственных данных» (которые можно использовать без дополнительных запросов внешних ресурсов). Такие объекты должны рассматриваться так же, как и любые другие внешние файлы. Использование встроенных изображений позволяет сэкономить HTTP-запросы к внешним ресурсам.
Хотя Opera 7.2+, Firefox, Safari, Netscape и Mozilla поддерживают data:URI, Internet Explorer 5–7 — решительно нет. Однако Internet Explorer 8 будет поддерживать эту схему. Существует также несколько приемов для поддержки старых версий Internet Explorer (о них чуть ниже).
Схема data:URI предоставляет способ для внедрения «непосредственно данных» точно так же, как если бы они были подключены через вызовы внешних файлов. Синтаксис у нее следующий:
data:[<тип данных>][;base64],<данные>
В случае простых изображений вам нужно указать mime-тип для них (например, image/gif), за ним идет base64-представление бинарного файла с изображением. Ниже приведен пример (переводы строк добавлены, чтобы не разрывать страницу, на самом деле их нет):
В результате мы получим следующее изображение иконки флага:
Такие изображения, внедренные в HTML-страницы, не кэшируются для повторного использования. И они не кэшируются от страницы к странице (это логично: ведь нам нужно каждый раз загружать HTML-код для отображения этой картинки, они будут кэшироваться только с HTML, их содержащим). Однако CSS-файлы замечательно кэшируются браузерами, и такие изображения могут быть повторно применены вместе с использующим их селектором, например:
ul {
list-style: none;
}
ul li {
margin: 0 0 1px;
background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAPJJREFUKM9tjz1OwlEQxH8P/hV2NIZY0NhYeA0TbkLLPTyFV6DgLhYWFIaOmEhM3szbtXhEPmSy2Z3d2Y9sORySEyKih87iCg4GYDIByEwoQGbPCowzR3mG3e576Jsz85zkLZRSIqIsFrlc5n5PBK1la0Rka2lfeDun07JafQ2bTTw/l+0WOy3klFLKWq/9fA4wADZS/g10ufdVpeqxYheIAehHq9Li1PrvgpQQw5rxk15/6mfYWR1yVIUc0pFUNync7vyw5m14gbHfQx+3l3di4Vba4z0MASOZ2Swl3LCQitQ/w8amtW4B5QBxZlymVxLwCz+JZR4AeSrEAAAAAElFTkSuQmCC) 0 0 no-repeat;)
height: 14px;
text-indent: 10px;
}
С описанным выше подходом для подключения изображений связаны две основные проблемы. Во-первых, вам нужно пересчитывать base64-представление изображений и редактировать CSS-файл каждый раз, когда само изображение меняется. Также IE до версии 7 включительно не поддерживает встроенных изображений. У первой проблемы есть простое решение на PHP:
Этот код читает файл с изображением и автоматически преобразовывает его на сервере в base64. Однако простота этого решения повлечет некоторую дополнительную нагрузку на сервер. Как вариант, можно рассмотреть автоматический пересчет всех картинок и вставку их в CSS-файл, например, раз в 5 минут по необходимости (если файл с изображением изменился). Дополнительно нужно будет озаботиться тем, чтобы сбросить кэширование для самого CSS-файла, содержащего такие изображения.
Существует три способа обойти отсутствие в IE поддержки data:URI. Используя распознавание браузеров (например, с помощью условных комментариев, ведь речь идет только про IE), можно просто отображать внешнее изображение для IE и встроенные изображения для остальных браузеров. Или вы можете применить JavaScript для эмуляции этой поддержки в IE, но эта техника потребует довольно значительного объема JavaScript-кода. О третьем способе пойдет речь в разделе, описывающем mhtml-технику.
Вышеприведенный PHP-код позволяет легко вставить base64-аналог изображения (можно расширить этот пример, чтобы, например, распознавать заголовки, отправляемые браузером серверу, и только для IE выводить URL для изображения, для остальных же кодировать его в base64):
ul {
list-style: none;
}
ul li {
margin: 0 0 1px;
background: url(data:image/gif;base64,
echo base64_encode(file_get_contents("../images/flag.png"))
?>) top left no-repeat;
height: 14px;
text-indent: 10px;
}
Когда сервер начнет анализировать CSS-файл, он автоматически перекодирует бинарный файл изображения в base64 и отправит эти данные внутри CSS-файла. Следующим шагом будет добавление распознавания браузеров для отправки изображения только IE и встроенных изображений всем остальным. Это можно сделать либо внутри CSS-файла с PHP-кодом, либо с помощью условных комментариев, например:
В файле ie.css должно быть нормальное обращение к картинке, например:
ul li {
margin: 0 0 1px;
background: url(/images/flag.png) 0 0 no-repeat;
}
...
Вместе с техникой CSS Sprites (или как ее альтернатива) data:URI может существенно уменьшить число HTTP-запросов. Краткий список плюсов данного метода:
Экономят HTTP-запросы, предотвращают издержки, связанные с большим числом объектов.
Экономят число параллельных потоков: у браузеров есть ограничение (по спецификации HTTP/1.1, однако Firefox, Opera и Safari несколько вольно его трактуют, в частности, позволяя настраивать этот параметр или значительно его увеличивая; о настройках браузеров можно прочитать в восьмой главе) на число одновременных соединений (подробнее об этом рассказывается в пятой главе) с одним хостом.
Упрощают HTTPS-запросы и улучшают производительность при таком типе соединения.
Однако встроенные изображения (только data:URI) не поддерживаются в Internet Explorer 5-7. Текстовое base64-представление данных также занимает больше, чем бинарное изображение. В наших тестах base64-данные были на 39-45% больше бинарного аналога, но gzip-сжатие позволяет уменьшить разницу до 5-10%. Предварительная оптимизация изображений перед base64-кодированием позволяет уменьшить их размер пропорционально (о сжатии изображений было рассказано во второй главе).
Также существует ряд ограничений на размер встроенных изображений. От браузеров требуется поддерживать только URL длиной до 1024 байтов, в соответствии с вышеупомянутой спецификацией RFC. Однако браузеры более либеральны к пользователям в том, что они принимают. Например, Opera и Firefox последних версий поддерживают data:URI примерно до 50 Кб (для IE8 этот предел составляет 32 Кб). Но все же эта техника подходит больше для небольших по размеру изображений. Краткий список минусов:
Не поддерживается IE до версии 7 включительно.
Требуются дополнительные действия для обновления внедренного содержания (перекодировать, еще раз вставить).
Ограничена длина. Не подходит для вставки больших изображений.
Изображения, представленные в base64-кодировке, примерно на 33% больше размера их бинарного аналога (на 10% — при использовании сжатия).
Встроенные картинки (не в CSS) не получится закэшировать по определению. Они будут кэшироваться только с HTML-кодом.
Одним из возможных выходов из сложившейся ситуации будет использование характерных для IE CSS-хаков, чтобы только для него подключить фоновые изображения. В итоге вышеприведенный пример будет выглядеть примерно так:
ul {
list-style: none;
}
ul li {
margin: 0 0 1px;
background: url(data:image/gif;base64,
echo base64_encode(file_get_contents("../images/flag.png "))
?>) top left no-repeat;
height: 14px;
text-indent: 10px;
}
* html ul li {
background-image: url(/images/flag.png);
}
*+html ul li {
background-image: url(/images/flag.png);
}
Также возможно кодирование изображений, которые выводятся в base64, автоматически при изменении этих изображений (для этого потребуется простой скрипт, который проверяет, обновились ли соответствующие файлы; если обновились, то перезаписывает их представление в CSS-файле, заодно и меняет хэш-строку для подключения этого файла в HTML, чтобы избежать кэширования).
Для включения небольших графиков прямо в HTML-код прекрасно подойдут условные комментарии, когда для ряда браузеров изображение выводится в base64, а для остальных (например, для IE) подключается через условные комментарии. Например, так:
Если использовать связку «относительное позиционирование родителя — абсолютное позиционирование дочернего элемента», то IE будет просто выводить картинку из внешнего файла поверх непонятного (для него) объекта.
IE (до версии 7 включительно) не поддерживает протокол data:URI, а вместе с ним base64-кодирование внешних файлов и включение их прямо в тело необходимого документа (будь то HTML или CSS/JavaScript-файл). Однако если рассмотреть использование протокола mhtml (который, конечно же, аккуратно поддерживается только в IE), многое становится более ясным, и base64-кодирование удается применять в полной мере.
В IE существует альтернативное решение для вставки изображений прямо в текстовый документ в виде mhtml-включений. Давайте остановимся на его практической реализации для CSS-файлов (в данном случае это файл main.css):
/*
Content-Type: multipart/related; boundary="_"
--_
Content-Location: 1
Content-Transfer-Encoding: base64
iVBOR..
*/
Далее в CSS-файле нужно лишь вызвать эту картинку следующим образом:
ul li {
background-image:url(mhtml:http://site.ru/main.css?20080531!1);
}
Здесь в адресе картинки идет протокол mhtml (который поддерживается исключительно в IE, но это не так важно), далее полный URL до CSS-файла (который содержит эту картинку), в данном случае этот URL еще и содержит GET-параметр для соответствующего кэширования. И небольшая тонкость применения данного формата: необходимо в URL использовать ту же строку, что и в HTML-файле, в котором подключается данный CSS, иначе IE запросит CSS-файл дважды: первый раз как таблицу стилей, второй раз — как хранилище картинки. Далее после восклицательного знака (!) идет тот идентификатор, который мы назначили картинке в Content-Location. И все.
С одной стороны, у нас схема data:URI, которая поддержана W3C и распознается всеми браузерами, кроме IE. С другой стороны, у нас IE, который понимает mhtml и с которым работают 70% наших пользователей. Мы объединим эти два решения, благо они оба используют base64-представление картинок.
Задача первая: объединить оба назначения стилевых правил, чтобы они не конфликтовали друг с другом. Решается это очень просто с помощью либо отдельного CSS-файла для IE (через условные комментарии), либо CSS-хаков (последнее предпочтительнее, ибо позволяет загружать всего один CSS-файл). В итоге в CSS-файле мы имеем примерно следующее:
/*
Content-Type: multipart/related; boundary="_"
--_
Content-Location: 1
Content-Transfer-Encoding: base64
iVBOR..
*/
ul li {
background: #fff url(data:image/png;base64,iVBOR...) 0 0 no-repeat;
}
* html ul li {
background-image: url(mhtml:http://site.ru/main.css?20081010!1);
}
*+html ul li {
background-image: url(mhtml:http://site.ru/main.css?20081010!1);
}
Данная конструкция позволяет вывести фоновое изображение в base64-кодировке для всех (ну или 99,9%) браузеров. Почему в конце содержатся 2 разных CSS-селектора с одним объявлением? Первое предназначено для IE6 и предыдущих версий, второе — для IE7. Объединить через запятую их нельзя.
Первый же скептический вопрос, который каждый читатель должен в этом месте задать: как можно эти картинки выводить только один раз в CSS-файле (можно заметить, что base64-строка фигурирует там дважды)? Ответ: никак. Однако выход есть.
Это gzip-сжатие. Если очень коротко, то при любом архивировании создается некоторый словарь наиболее часто используемых строк, которые заменяются на однобайтовые представления. При восстановлении данных из архива происходит обратная процедура, когда значения слов словаря подставляются вместо их наименований. В нашем случае у нас для одной картинки используется одна и та же base64-строка. Это означает, что при архивировании она сожмется удивительно эффективно.
Стоит также заметить, что при таком подходе результирующий сжатый файл занимает даже меньше, чем при назначении с помощью CSS-хаков для IE внешних изображений для загрузки (ведь используется меньше различных конструкций).
Получившийся таким образом CSS-файл абсолютно валиден (так как все mhtml-вставки происходят в комментариях). Каждое CSS-объявление валидно с точки зрения CSS 2.1, и те небольшие приемы, которые позволяют эффективно сжать данные для всех браузеров, не отражаются на восприятии ими файла. Такой подход будет уместным только для CSS. Для HTML ситуация совсем другая (о ней немного подробнее ниже). HTML-документ при таком подходе оказывается невалидным, что выливается во множественные проблемы верстки, и это не та же цена, которую можно заплатить за некоторый не очень явный рост производительности.
Картинки в CSS можно вставлять с помощью data:URI.
Для IE можно использовать mhtml, полностью дублирующий эту функциональность.
Для корректного применения стилей нужны CSS-хаки либо разделение CSS-файлов.
Используем gzip для CSS-файлов для устранения последствий множественного использования base64-строки.
Использовать base64-кодирование на ваших страницах можно уже прямо сегодня. Очень важно при этом понимать возможные последствия и по возможности обойтись малой кровью. Однако с выходом IE8 и массовым его распространением ситуация кардинально изменится (ведь он поддерживает data:URI и для него описанные обходные приемы уже теряют свою актуальность).
Летом 2008 года весь мир облетела страница, содержащая реализацию первого уровня Super Mario Brothers. В нее, в общем, можно играть, хотя упущены многие ключевые аспекты (нет грибов, нет флага, нет повышающих очков и т. д.). Однако это, на самом деле, не самый интересный аспект этой разработки.
Наверное, больше всего интереса в этом веб-приложении представляет не сама игровая механика, а тот факт, что все собрано в одном-единственном файле — игровом скрипте, который включает все игровые спрайты и всю музыку. Это весьма любопытный образец использования технологий. Удивительно, но способ подключения музыки в этом приложении, пожалуй, наиболее простой для понимания. Приложение использует схему data:URI, которая кодирует музыкальные MIDI-файлы Mario в base64-виде.
Результат очень просто найти в исходном файле:
aSounds = [
// очень маленькая и простая тема Mario. Написал Mike Martel.
"data:audio/mid;base64,TVRoZAAAAAYAAQAEAMBNVH...",
// игра закончилась. Написал John N. Engelmann.
"data:audio/mid;base64,TVRoZAAAAAYAAQADAHhNVH..."
],
data:URI работает следующим образом: все содержимое выбранного файла кодируется в виде одной строки в исходном файле. В результате мы получаем 1 файл вместо трех: одного исходного и двух внешних. В данном случае вся закодированная строка отправляется элементу , который и отвечает за проигрывание MIDI-файла. Можно обнаружить конечный результат этих махинаций в следующем виде:
playMusic = function(iSoundID, bLoop) {
if (!bMusic)
return;
var oEmbed = dc("embed");
oEmbed.src = aSounds[iSoundID];
oEmbed.id = "sound_" + iSoundID;
if (bLoop)
oEmbed.setAttribute("loop", "true");
oEmbed.setAttribute("autostart", "true");
oEmbed.style.position = "absolute";
oEmbed.style.left = -1000;
appChild(document.body, oEmbed);
},
Вышеприведенный код просто создает элемент embed, устанавливает для него автозапуск проигрывания музыки и в качестве источника данных указывает data:URI. В результате мы получаем MIDI-файл, который сам начинает проигрываться (предположительно, тот же результат мог быть получен при использовании другого универсального музыкального файлового формата — WAV).
Файлы в формате data:URI могут проигрываться всеми браузерами, за исключением Internet Explorer. Поэтому пользователи Internet Explorer просто не получают музыкального сопровождения для игры (хотя можно было бы только для IE подгружать его динамически, создавая тот же самый embed просто со ссылкой на внешний файл).
Давайте проведем сравнение двух освещенных выше методов для кардинального уменьшения числа запрашиваемых файлов с сервера: CSS Sprites и data:URI.
С какими проблемами сталкивается верстальщик, когда использует CSS Sprites? Это, в первую очередь, проблемы изменения каждой конкретной картинки в общем массиве. Для этого нужно открыть ресурсную картинку, найти в ней область, соответствующую данному небольшому изображению (которое меняется), и заменить ее, не потеряв палитру при всех изменениях. Также при изменении расположения картинок в ресурсном файле (например, перераспределили свободное место в связи с очередными дизайнерскими изменениями) нужно заново пересчитать все координаты и внести соответствующие изменения в CSS-файл.
При небольшом количестве спрайтов или их равномерном распределении (например, иконок для пунктов меню) это будет не очень сложно. Но если ресурсный файл представляет собой набор картинок разных размеров, например как у Google? Тогда любое изменение может превратиться в ночной кошмар. В любом случае, на использование спрайтов тратится дополнительное время при разработке сайтов.
Также у IE возникают проблемы с позиционированием полупрозрачных PNG-картинок (которые нужно вставлять через AlpaImageLoader). Таким образом, больше одной такой картинки в спрайт не добавить (в левый верхний угол). Это можно обойти при помощи IE-фильтра crop или абсолютного позиционирования и дополнительной разметки (когда в контейнер с относительным позиционированием вставляем изображение с абсолютным позиционированием и накладываем поверх этого изображения все остальное содержимое контейнера — оно будет располагаться в нем в обычном порядке).
Казалось бы, CSS Sprites призваны уменьшить задержку при загрузке страницы, однако на практике так происходит только при правильном подходе. Обычно в ресурсную картинку объединяется все подряд, картинка многократно увеличивается, а визуальная задержка при загрузке сайта только растет: пользователь может ждать 1 большую картинку дольше, чем половину входящих в нее маленьких, а последние обеспечат ему «почти» загрузку сайта.
Если спрайтов у нас немного (1–2 картинки), то общее время загрузки сайта, скорее всего, возрастет. Это связано с тем, что браузер не сможет открыть как минимум 4-8 дополнительных соединений (к хостам, где расположена статика) и загрузить все исходные картинки параллельно, а не последовательно. При небольшой сетевой задержке это может оказать решающее воздействие.
Даже если положиться на то, что спрайты поддерживаются (почти) всеми браузерами на данный момент, все равно остается достаточно много вопросов, которые они не только не решают, а скорее сами создают. Во-первых, это проблемы при использовании иконок для списка. В таком случае необходимо располагать маленькие картинки «лесенкой», но в общем случае это увеличивает размер получившейся картинки на 20-30%.
Во-вторых, при использовании спрайтов, когда пытаются обойти обрисованную проблему с «выползанием» фона для ненужных элементов, в структуре страницы появляются несемантические элементы (дополнительная разметка), которые, по сути, заменяют тег img, только через фоновое изображение. Такое засорение структуры страницы вредно отражается на всех уровнях разработки и использования сайта.
В-третьих, если логотип склеен с другими картинками (как у вышеуказанного примера), то становится невозможным его использование как обычной картинки (например, в версии для печати или на внешних ресурсах).
С развитием техник data:URI наиболее логичным выходом из сложившейся ситуации будет следующий характер разработки:
Верстальщик создает рабочую версию макета сайта, не прибегая к помощи CSS Sprites (на каждый элемент — одна фоновая картинка).
Веб-технолог внедряет кодирование картинок в base64 (+mhtml) в CSS-файл(ы) на этапе их загрузки на боевой сайт, создавая автоматизированное решение. На этом этапе могут использоваться и CSS Sprites, однако их внедрять сложнее из-за перерасчета позиционирования фона и обновления соответствующих стилевых правил.
Веб-программист обеспечивает для ряда «старых» браузеров загрузку версии без использования data:URI.
Чем это хорошо? Верстальщик не думает лишний раз, что и как ему расположить и нарезать: эти операции уже включены в процесс публикации сайта, автоматизированы и максимально адаптированы под пользователей.
Чем это плохо? В общем случае, загрузка страницы не ускорится, а даже может замедлиться, потому что фоновые картинки (включенные через data:URI) будут грузиться в один поток, а не в несколько, как при обычном использовании спрайтов. Если фоновых картинок достаточно много (несколько десятков килобайтов), то это окажется существенным. При небольшом их объеме (до 10 Кб) будет заметно явное ускорение.
Как распределить загрузку фоновых картинок между предзагрузкой и пост-загрузкой (фактически ускорив первую за счет замедления второй), рассказывается чуть ниже.
function combinedWindowOnload() {
load_dynamic_css("background-images.css");
...
}
/* если мы сможем создать динамический файл стилей */
if (document.getElementsByTagName) {
/* то добавляем в загрузку облегченную версию */
document.write('\x3clink href="light-light.css"
rel="stylesheet" type="text/css" media="all"/>');
/* после этого начинаем HTML-комментарий */
document.write('\x3c--');
}
Внутри или снаружи?
Давайте в качестве заключения рассмотрим следующий вопрос: стоит ли вообще подключать JavaScript- и CSS-файлы или можно включить весь их код непосредственно в код страницы?
Использование подключаемых файлов на практике обычно дает более быстрые страницы, т. к. браузеры кэшируют файлы скриптов и CSS. JavaScript- и CSS-код, который находится в HTML, загружается каждый раз при загрузке самого HTML-документа. Это уменьшает количество необходимых HTTP-запросов, но увеличивает объем HTML. С другой стороны, если скрипты и таблицы стилей находятся в отдельных файлах, закэшированных браузером, размер HTML уменьшается, не увеличивая при этом количество HTTP-запросов (при повторных посещениях).
В таком случае ключевым фактором является частота, с которой кэшируются внешние JavaScript- и CSS-файлы относительно количества запросов самого HTML-документа. И хотя этот фактор очень сложно посчитать, его можно приблизительно оценить различными способами. Если пользователи во время одного посещения загружают страницу несколько раз или загружают похожие страницы, которые используют один и тот же код, — это именно тот случай, когда мы можем получить все преимущества от вынесения кода в отдельные файлы.
Многие сайты только наполовину удовлетворяют этим условиям. Для таких случаев в целом лучшим решением будет создание внешних файлов скриптов и таблиц стилей. Единственное исключение, которое можно здесь привести (когда прямое добавление кода дает большое преимущество) — это использование его на главных страницах, таких как главная страница Яндекса ( http://www.yandex.ru/ ), Рамблера ( http://www.rambler.ru/ ) или Google ( http://www.google.ru/ ). Для страниц, которые загружаются всего несколько (обычно — один) раз за весь сеанс, выгодней включать скрипты и таблицы стилей прямо в HTML-документ, чтобы выиграть в скорости загрузки.
Для таких главных страниц, которые открываются первыми в ряду других с этого же сайта, существует возможность уменьшить число HTTP-запросов еще и следующим образом: мы можем включить JavaScript и CSS в код самой страницы, однако после ее полной загрузки динамически подгружать внешние файлы стилей и скриптов для последующего использования (на стадии пост-загрузки). При этом следующие страницы будут использовать уже закэшированные файлы.