Статьи

Автор: Мациевский Николай aka sunnybear
Опубликована: 6 октября 2008

Выносим CSS в пост-загрузку

После сравнительной заметки о CSS Sprites и data:URL все мои мысли были направлены на решение основной проблемы:

В общем случае [при использовании data:URL], загрузка страницы не ускорится, а даже может замедлиться, потому что фоновые картинки (включенные через data:URL) будут грузиться в один поток, а не в несколько при обычном использовании спрайтов. Если фоновых картинок достаточно много (несколько десятков Кб), то это окажется существенным.

Данная статья как раз посвящена тому, как можно достаточно успешно справиться с указанной проблемой. Интересно? Тогда, поехали.

Постановка задачи

При использовании data:URL итоговый CSS-файл занимает довольно большой объем (фактически, размер всех картинок*1,2...1,5 + базовый CSS). И это в виде архива. Если файл не заархивирован, то его дополнительный размер увеличивается многократно (в 2,5–3 раза относительно размера всех фоновых изображений), но это не так существенно, ибо пользователей с отключенным сжатием для CSS-файлов сейчас единицы.

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

Чего мы таким образом добиваемся? Фактически, используя такой подход, мы создаем другой контейнер для фоновых изображений (не ресурсное изображение, а CSS-файл), который удобнее использовать в большинстве случаев. Мы объединяем все фоновые картинки не через CSS Sprites, а через data:URL, и можем загрузить их все одним файлом (в котором каждая картинка будет храниться совсем отдельно). При этом избегаем любых проблем с позиционированием фона.

Теоретическое решение

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

Возможные минусы: после загрузки каждого дополнительного CSS-файла будет перерисовка страницы. Если таких файлов всего 1 или 2, то отображение страницы произойдет значительно быстрее.

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

На практике

На практике все оказалось не сильно сложнее. Мы загружаем в head страницы (до вызовов любых внешних файлов) наш «легкий» CSS:

<link href="light-light.css" rel="stylesheet" type="text/css" media="all"/>

А затем добавляем в комбинированный window.onload (в самое начало) создание нового файла стилей, который дополняет уже загрузившийся фоновыми изображениями:

function combinedWindowOnload() {
    load_dynamic_css("background-images.css");
    ...
}

В результате мы имеем максимально быстрое отображение страницы, а затем стадию пост-загрузки, которая вытянет и с сервера все дополнительные картинки (тут уже сам браузер постарается), стилевые правила и скрипты.

А доступность?

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

С маленьким нюансом: noscript не может находиться в head, а link не может находиться в body. Если мы соблюдаем стандарты (все же иногда лучше довериться профессионалам и не ставить браузеры в неудобное положение, когда они встретятся с очередным отклонением от спецификации), то стоит искать обходные пути.

После небольших экспериментов с коллекцией styleSheets и другими DOM-объектами, было выделено следующее изящное решение, обеспечивающее работу схемы во всех браузерах (замечание: после многоличсленных экспериментах было решено остановиться на HTML-комментариях: они оказались наилучшим способом запретить загрузку указанного CSS-файла):

<script type="text/javascript">
/* если мы сможем создать динамический файл стилей */
if (document.getElementsByTagName) {
/* то добавляем в загрузку облегченную версию */
document.write('\x3clink href="light-light.css" rel="stylesheet" type="text/css" media="all"/>');
/* после этого начинаем HTML-комментарий */
document.write('\x3c--');
}
</script>
<link href="full.css" rel="stylesheet" type="text/css" media="all"/>
<!--[if IE]><![endif]-->

В результате брайзер с включенным JavaScript запишет начало комментария, а закроет его только после <link> (комментарии не могут быть вложенными). При выключенном JavaScript <script> не отработает, <link> обработается и добавится в очередь загрузки, а последний комментарий будет просто комментарием.

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

Грабли, грабли, грабли...

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

setTimeout('load_dynamic_css("background-images.css")',0);

В Safari же логика отображения страницы в зависимости от загружаемых файлов отличается ото всех браузеров. Подробнее можно прочитать эту заметку о FOUC проблеме и возможных решениях. Если в двух словах, то можно жестко определить начальный набор файлов, необходимых для отображения страницы на экране (HTML/CSS/JS), а можно начать загружать все файлы в порядке приоритетности (и выполняя все их зависимости) и проверять время от времени, можно ли уже отобразить страницу (выполняя все вычисления в фоновом режиме без обновления экрана). В общем, у Safari второй подход, поэтому ничего лучше выноса загрузки динамического CSS-файла с фоновыми картинками после срабатывания window.onload (который, на самом деле, срабатывает раньше, чем во всех остальных браузерах) придумать не удалось. Зато первоначальная картинка в браузере появляется быстрее.

Итак, если брать сразу комбинированный window.onload и добавлять в него загрузку стилей, то получится примерно такая конструкция:

/*
объявляем функцию по динамической загрузке стилей и скриптов
первый параметр - путь к файлу, второй - тип файла
К сожалению, функция объявлена глобальна
*/
function loadDynamic (src,type){
    var node=document.createElement(type?"link":"script");
    node = document.getElementsByTagName("head")[0].appendChild(node);
/*
если передаем второй параметр, то это таблица стилей
*/
    if(type){
	node.setAttribute("rel","stylesheet");
	node.setAttribute("media","all")
    }
    node.setAttribute("type","text/"+(type?"css":"javascript"));
    node.setAttribute(type?"href":"src",src)
}
(function(){
/*
немного модифицированная версия обработчика
https://webo.in/articles/habrahabr/05-delayed-loading/
*/
    function combinedWindowOnload(){
	if(arguments.callee.done){return}
	arguments.callee.done=true;
	if(document.getElementsByTagName){
/* 
если не Safari, то загружаем CSS с фоновыми изображениями динамически
*/
	    if(!/WebKit/i.test(navigator.userAgent)){
/*
для обхода IE добавляем псевдо-задержку
*/
		setTimeout('loadDynamic("background-images.css",1)',0);
	    }
/* 
ставим на поток загрузку всех наших скриптов
*/
	    loadDynamic("some_scripts.js");
        }
    }
/* 
дальше идет стандартный кроссбраузерный код с
https://webo.in/articles/habrahabr/05-delayed-loading/
*/
...
/* 
навешиваем на window обработчик по событию Onload, спасибо lusever за компактный вид
https://webo.in/articles/livejournal/01-native-browser-events/
*/
    window[/*@cc_on !@*/0?'attachEvent':'addEventListener'](/*@cc_on 'on'+@*/'load',function(){
/* 
если Safari, то загружаем наконец этот CSS
*/
        if(/WebKit/i.test(navigator.userAgent)){
            loadDynamic("background-images.css",1);
        }
/*
добавочный вызов WindowOnload для "старых" браузеров
*/
        combinedWindowOnload()
    },false)
})()

Выигрыш

При наличии у вас большого количества маленьких декоративных фоновых изображений (например, для webo.in на каждой странице используется от 30 до 40 различных картинок, общий объем которых составляем порядка 30Кб), которые к тому же могут повторяться по различных направлениям, может быть очень удобно объединить их все в один файл, который загружать после отображения страницы на экране.

Описанная техника (кроссбраузерный data:URL + динамическая загрузка файлов стилей) позволяет добиться всех преимуществ технологии CSS Sprites, не затягивая загрузку страницы. При этом обладает очевидными плюсами: не нужно лепить все картинки в один файл, можно работать с каждой совершенно отдельно, что позволяет добиться большей семантичности кода и большего удобства использования сайтов. к тому же это несколько сократит CSS-код за счет уничтожения необходимости использовать background-position.

Примеры загрузки

Во всех CSS файлы добавлена некоторая задержка для демонстрации функционирования метода на медленных соединениях (или при больших объемах файлов).

Выигрыш на реальном соединении (стадия предзагрузки страницы) будет равен размеру всех CSS-изображений (для webo.in этот код составляет 26Кб в непожатом и 8,5Кб в архивированном виде, соответственно, для 100Кб/с это будет 260..85мс). По-прежнему отдельным файлом загружается CSS Sprites в 19Кб.

Вся стадия предзагрузки отображается, учитывая пожатый размер HTML в 24,5Кб:

(DNS) + (TCP/IP) + (HTML) + (CSS) ~= 100мс + 100мс + 245мс + 125 мс ~= 570мс.

Выигрыш при применении метода составит 12%. Все расчеты проведены для главной страницы webo.in.

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

P.S. я посмотрел решение от YUI по загрузке файлов стилей. Оно решает другую задачу: как вообще загрузить несколько дополнительных CSS-файлов на страницу, а не как загрузить их максимально быстро.

Читать дальше

Все комментарии (habrahabr.ru)