Однажды я совершил страшное дело – посмотрел исходный код главной страницы Яндекса и задумался – что-то тут не так. Я вижу 5 заголовков последних новостей, несколько картинок, поле поиска, ссылочки и вот вопрос:
Где тут 700Кб, которые требуют 1,56с моего времени? Они в коде.
Все мы ходим по сайтам, получаем килобайт полезной для себя или вообще воспринимаемой человеком информации и при скорости в 3Мбит/с тратим на это секунды. В этом посте я постараюсь описать, как передавать как раз только тот килобайт, который нужен человеку, а все остальное либо кешировать, либо вообще грузить только когда понадобится.
Первый на очереди к оптимизации – HTML
Было бы неплохо кешировать весь HTML или хотя бы его большую часть – для этого можно использовать блочное кеширование: страницу разбиваем на блоки и эти блоки выносим в отдельный JS файл, который потом остается у пользователя. Плюс такого подхода в простоте, но в нашем случае будет нетривиальное решение – надо все немного усовершенствовать.
Страницу можно не просто разбивать на блоки, а все в body заменить на XML структуру, соблюдая все отношения родительства – радикально и эффективно. Никаких стилей и излишеств. Для примера возьму код Яндекса для вывода 5-ти новостей:
<div class="b-tabs__items">
<div id="tabs088905610376_0">
<ul class="b-news-list">
<li class="b-news-list__item">
<span class="b-news-list__item_num">1.</span>
Главная военная прокуратура <a class="b-news-list__item-link" href="http://news.yandex.ru/yandsearch?cl4url=www.rg.ru%2F2014%2F05%2F06%2Fserdukov-anons.html&lang=ru&lr=172" onmousedown="cp('v12.news.news.links.1',this)">признала законной амнистию Сердюкова</a>
</li>
...
</ul></div></div>
Получилось следующее:
<novosti>
<tab ids="tabs088905610376_0">
<newsr num="1" pre="Главная военная прокуратура" src="http://news.yandex.ru/yandsearch?cl4url=www.rg.ru%2F2014%2F05%2F06%2Fserdukov-anons.html&lang=ru&lr=172" onmd="cp('v12.news.news.links.1',this)" ahr="признала законной амнистию Сердюкова">
...
</tab>
...
</novosti>
Каждая новость теперь становится одним тегом. Если подобным образом представить всю страницу, то получится как раз некая помесь HTML и XML, можно назвать это [X]HTML, потому что фактически это HTML, только теперь он расширяемый – в прямом смысле этого слова. Допустим страница представлена в XML, теперь для каждого блока я добавляю в JS файл его шаблон – то, как он должен выглядеть на самом деле. По аналогии с PHP я выделил ‘$’ в шаблонах переменные, которые надо брать из атрибутов и $HTML$ – innerHTML тега.
var novosti='<div class="b-tabs__items">$HTML$</div>';
var tab='<div id="$ids$"><ul class="b-news-list">$HTML$</ul></div>';
var newsr='<li class="b-news-list__item"><span class="b-news-list__item_num">$num$.</span> $pre$ <a class="b-news-list__item-link" href="$src$" onmousedown="$onmd$">$ahr</a></li>';
Итог – весь HTML помещается в JS файл, а на самой странице остается только то, что нужно пользователю, но это только половина дела, потому что основной трафик страницы, помимо изображений, это JS.
Оптимизация JS
Если JS файл весит 100-200Кб, то его загрузка это около 0,5с. Он загрузится и останется в кеше, а вот задержка перед его выполнением все равно останется – это связано с тем, что JS файл сначала загружается, потом весь анализируется и только после этого начитает выполняться. Такая особенность позволяет, например, вызвать функцию еще до ее объявления. Чем больше файл – тем дольше аналаз, до 300мс, а иногда и больше. Есть очень хороший способ сократить время — сделать файл дискретным: разбить на отдельные функции или просто части и подключать их, когда они нужны. Они так же будут кешироваться, только время их анализа будет гораздо меньше – в пределах 1-2мс.
Пусть у нас есть функция, которая в реальном времени подключает JS файлы, например, вот такого вида:
inc({
//список подключаемых функций/файлов вида name:'id' – name-название функции (для вызова), id-постфикс для файла
},function() {
//JS с использованием подключенных функций и файлов
});
Для нее есть несколько требований. Во-первых, она должна запоминать уже подключенные функции и файлы, во-вторых поддерживать загрузку из разных каталогов, а так же быть асинхронной. Для указания каталога можно использовать глобальную переменную includesrc=«любойсайт.ру/каталог/» для подключения файлов и «любойсайт.ру/каталог/функция_» для функций – к этому пути будет добавляться id (постфикс). Допустим, что надо разбить файл на несколько частей или функций, тогда полученные файлы должны быть примерно такого вида:
//если файл с одной функцией, то
ludes["id"]=function(a1, .., aN){
//код
}
//если с несколькими:
//функции (просто часть исходного файла)
ludes["id"]++;//id - это имя файла, без расширения
Таким образом, в файле с функцией должна быть указана часть имени файла, а при подключении файла — его название, только без расширения.
ESS
Выше я написал, как можно вынести весь HTML и кешировать, передавать только нужную информацию в XML виде, потом написано, как можно свести задержку перед выполнением JS к нескольким миллисекундам, но нет самого решения проблемы – вот оно. Его можно назвать ESS – Easy Stylesheets, по аналогии с CSS. Состоит из 2+1 функций.
ess(a, b); – функция, заменяющая XML блоки на странице шаблонами из JS файла, ей надо передать a — элемент DOM и b — строчку-шаблон.
inc(); – уже упомянутая функция, которая позволяет асинхронно подключить любой файл, с любого домена и затем продолжить работу.
И вспомогательная функция g(a, b, c); – ищет объект по строчкам CSS вида. a — с каким tag/#id/@ name/.class искать объект, b — в каком объекте искать и с — флаг 0/1, указывающий, надо ли в любом случае возвращать массив.
Для ESS есть документация, в которой описан синтаксис и наглядные примеры использования. Функции ess() и inc() не только позволяют экономить время пользователя и снижает нагрузку на сервер, они так же открывают новые возможности для программирования на JS, но это уже тема для отдельной статьи.