С появлением мобильного веба наш интернет стал снова плохим, а устройства медленными. 3G, 4G, Wi-Fi… — они, конечно, где-то есть, но когда очень надо, то как правило скорость падает до околомодемной и получается, что наши мобильный устройства «каменного века» попадают в условия современного объема информации. Даже в центре города (правда на 15-м этаже) значек мобильного интернета может показывать волшебную букву Е, намекающую о том, что уж лучше не тратить нервы и потерпеть. Лучше уж использовать нативную версию какого-то веб-сервиса, чем каждый раз ждать, загружать по мегабайту, чтобы отправить короткое сообщение. Нативную версию веб-сервиса... Понятное дело маркетинг, гонка приложений. Однако, же пользователи выбирают нативные веб-приложения, которые работают быстрее, не качают кучу ресурсов, хотя им приходится периодически его обновлять.
Эта статья о том какими путями можно оптимизировать загрузку и инициализацию JavaScript.
Не весь код используется
Я провел небольшое исследование, призванное выявить объем кода, который используется при заходе пользователя на первую страницу различных мобильных версий популярных сайтов. Для этого я использовал Chrome + script-cover плагин.
В среднем по больнице используется около 40 процентов кода. Есть сайты, которые используют и 80% загружаемых ресурсов, но есть и те, которые используют всего 20%. Вы будете правы если скажете, что «остальной то код будет использоваться из кэша на других страницах» — мы как разработчики знаем зачем грузится столько лишнего, а простой пользователь каждый раз ждет когда же загрузится весь этот объем, хотя ему нужно всего-то отправить сообщение своим друзьям.
Кэш
Предположим, что пользователь один раз скачал все наши ресурсы, они попали в кэш и в следующий раз он не будет их загружать (Expires: +300 years FTW).
Сейчас объем кэша десктопного веб-браузера 40-80Мб. Этот объем можно растратить за час-полтора активного веб серфинга, потому как практически каждый сайт желает пролезть в кэш со своими Expires: +300 years на картинки и другие сомнительные ресурсы и вытеснить ваши полезные скрипты. Получается своеобразный царь горы или индийская электричка. Даже вкрутив объем дискового кэша в 400 Мб можно каждый день выкачивать те же скрипты и стили.
С мобильными все хуже — iOS Safari не имеет дискового кэша (только в памяти), у Андроида он ограничен 20Мб.
Приложение для оптимизации
Я создал одно простое приложение — это прототип чата. Мы будем пытаться ускорить его загрузку в очень суровых условиях — на скорости интернета в 7Кб/с. Все его версии можно посмотреть вот тут azproduction.github.com/loader-test/
Последовательная загрузка и исполнение
При старте код приложения, который подгружает наши скрипты, выглядит примерно вот так:
<script src="js/b-roster.js"></script>
<script src="js/b-dialog.js"></script>
<script src="js/b-talk.js"></script>
<script src="js/index.js"></script>
Наши скрипты загружаются и запускаются последовательно (на самом деле в современных браузерах это немного не так), 4 запроса. Безусловно, это самый худший из всех вариантов. Его профиль выглядит вот так:
18 секунд… столько не ждут…
Параллельная загрузка и исполнение
Немного изменим наш код, добавив атрибут async, чтобы наши скрипты грузились и стартовали параллельно. Для многих приложений такой способ загрузки не будет работать т.к. скрипты могут иметь зависимости от предыдущего кода.
<script src="js/b-roster.js" async></script>
<script src="js/b-dialog.js" async></script>
<script src="js/b-talk.js" async></script>
<script src="js/index.js" async></script>
Посмотрим, что же мы получим
Ничего не изменилось… только событие DOM ready срабатывает немного раньше, но нам это не поможет т.к. нам нужен весь код для работы приложения.
Параллельная загрузка, последовательный запуск
Попробуем применить другую «оптимизацию» будем параллельно загружать, но последовательно запускать. Для этого воспользуемся библиотечкой LAB.js:
<script type="text/javascript" src="vendors/LAB.min.js"></script>
<script>
$LAB
.script("js/b-roster.js")
.script("js/b-dialog.js")
.script("js/b-talk.js")
.wait()
.script("js/index.js");
</script>
А стало только хуже:
Казалось бы грузим параллельно — значит загрузка ресурсов должна быть не блокирующая, и все должно быть немного, но быстрее.
На самом деле все современные браузеры грузят все скрипты на странице параллельно, но запускают их последовательно в порядке декларации в документе. Если какой-то скрипт пришел раньше, чем нужно, то его запуск блокируется. В случае LAB.js мы фактически делаем работу браузера, притом, что скрипт LAB.js блокирует загрузку всех остальных скриптов, да еще и занимает какой-то объем.
Собираем и пакуем
Применим другую достаточно очевидную оптимизацию — соберем все скрипты в 1 файл и сожмем этот код каким-нибудь минификатором.
$ cat **/*.js > main.js
$ java -jar yuicompressor.jar main.js -o main.min.js
Думаю, для многих эти строчки знакомы, я использовать YUI Compressor, но советовал бы использовать UglifyJs или Closure Compiler
Результат этой оптимизации предельно очевиден. В принципе, можно дальше и не оптимизировать :) Но! 9с… — столько пользователи не будут каждый раз ждать.
AppCache — оффлайн хранилище
В отличии от общего кэша этот является личным для каждого приложения и другие приложения не смогут вытеснить его рессурсы. Единственно, что может произойти — это закончится общая квота на AppCache или пользователь его очистит. AppCache поддерживается многоми браузерами. Хотя он и называется оффлайн хранилище — но его ресурсы мы можем использовать для работы онлайн.
Подключить его очень просто:
Достаточно прописать атрибут manifest с ссылкой на appcache файл
<html manifest="example.appcache">
</html>
Создать этот файл с перечислением всех ресурсов, которые должны попасть в кэш (это самый простой вариант файла)
CACHE MANIFEST
# v1 - 2011-08-13
http://example.com/index.html
http://example.com/main.js
И в настройках вашего веб-сервера прописать несколько строк, чтобы файл отдавался с правильным MIME-типом и не кэшировался
AddType text/cache-manifest .appcache
ExpiresByType text/cache-manifest "access plus 0 seconds"
Используя AppCache вы можете своевременно (без лишних костылей) сообщить пользователю, что его приложение устарело и попросить перезагрузить страницу, либо спросить резрешение на перезагрузку. Всю сетевую активность и проверку кэша берет на себе браузер — вам нужно всего лишь подписаться на событие updateready и проверить стутус кэша.
window.applicationCache.addEventListener('updateready', function () {
// делаем что-нибудь
});
// Или проверить его статус при старте
if(window.applicationCache.status === window.applicationCache.UPDATEREADY) {
// делаем что-нибудь
}
Плюсы AppCache
1. Надежное кэширование
2. Работа оффлайн
3. Простое управление версиями
4. Своевременное обновление
Минусы AppCache
1. Может закончиться дисковая квота
2. Пользователь может не разрешить вашему сайту использовать кэш (в случае с Firefox)
Думаю предельно очевидно какие результаты мы получим при повторной загрузке приложения из кэша — 0 запросов, 0 байт. (1 запрос может уйти на загрузку файла .appcache)
Подробнее о AppCache
Статья о AppCache на MDN tinyurl.com/mdn-appcache
FAQ по AppCache appcachefacts.info/
AppCache для новичков www.html5rocks.com/en/tutorials/appcache/beginner/
Выборочная загрузка
Хотя сейчас у нас есть хорошее кэширование, но загрузка нашего приложения далеко не оптимальна. Мы все еще грузим лишние ресурсы, которые, возможно, и не нужны пользователю сейчас. Применим оптимизацию с ленивой загрузкой скриптов. Мы можем воспользоваться «паттерном» AMD — Asynchronous Module Definition и библиотекой RequireJS, которая реализует этот API
1. Грузим основные части
2. Остальное по необходимости
3. Автодогрузка зависимостей
4.…
5. PROFIT
Мы можем поделить наше приложение на 2 части — это ростер со списком контактов и диалог. Ростер должен быть всегда показан, а диалог открывается реже, поэтому мы будем его грузить по необходимости.
Наш html стал вот такой
<script data-main="js/amd/index" src="vendors/require.js"></script>
Каждый модуль мы обернули в необходимую для require.js обертку. В случае index.js она будет вот такой:
require(["b-roster"], function(Roster) {
new Roster($('body'));
});
А каждый загружаемый модуль мы обернем в другую обертку:
define(function () {
// Тут какой-то код модуля
return ModuleName;
});
При старте мы загружаем только index.js и roster.js, а по клику на элемент ростера подгружаем остальные файлы:
querySelector('.b-roster').addEventListener('click', function (e) {
require(["b-dialog"], function(Dialog) {
new Dialog(element);
});
}, false);
Идея и реализация проста, посмотрим, что же мы получили в итоге:
По сравнению с предыдущим результатом мы стали делать на 2 запроса больше, однако первоначальный объем скриптов снизился на 16.5Кб, время на 2.1с
У этого подхода, безусловно, есть один существенный минус — пользователю по клику на элемент ростера приходится ждать 4 секунды (чтобы загрузились остальные скрипты), что, конечно, не очень хорошо.
Ленивая загрузка и инициализация
7.4 секунды при старте — это, конечно, уже не 18, но и не 3-5 секунд, которые пользователь может потерпеть.
При увеличении объема скриптов растет и время старта приложения — Startup Latency. Дело в том, что при старте нашему браузеру приходится интерпретировать и инициализировать все функции, объекты, конструкторы, которые были перечислены в этом файле, даже если они нам совершенно не нужны в данный момент. А время инициализации 1Мб пожатого JavaScript может доходить до 3 секунд в зависимости от браузера и загруженности ресурсов устройства. Для десктопных браузеров эта цифра, конечно, значительно ниже (100-300мс).
При сборке нашего приложения мы можем трансформировать наши скрипты в строки, а потом по необходимости отэвалить их и получить код. Да, eval медленнее, чем обычный запуск, но эта инициализация происходит не во время старта, а во время работы приложения, что позволяет запускаться нашему приложению быстрее.
LMD
На основе этой идеи я создал принцип LMD — Lazy Module Declaration и сделал еще один «загрузчик» с одноименным названием. Кроме ленивой инициализации LMD имеет и ряд других преимуществ по сравнению с другими:
2. Node.js-подобные модули
AMD требует от нас каждый раз писать define(), хотя мы можем обойтись без define и писать код для браузера 1 в 1 как для Node.js без каких-либо оберток и магии с экспортами.
Вот так выглядит код index.js под LMD:
var $ = require().$, // require("undefined")
Roster = require("b-roster");
new Roster($('body'));
LMD при сборке LMD-пакета уже сам оборачивает данный код в необходимую для него обертку.
3. Встроенный сборщик и упаковщик
В LMD уже встроен сборщик ваших скриптов и упаковщик. Вам достаточно задекларировать все ваши скрипты, которые должны войти в пакет:
{
"path": "./modules/",
"modules": {
"main": "index.js",
"b-roster": "b-roster.js",
"undefined": "utils.js",
// А еще можно использовать *
"*": "*.js"
}
}
LMD их сожмет и упакует, однако сжатие и упаковка опциональны — вы можете управлять ими для всего файла или для каждого модуля отдельно.
4. Гибкий объем библиотеки
В отличии от Require.js-AMD весь код LMD входит в ваш пакет скриптов уже на момент сборки без каких-либо лишних телодвижений. Вы можете гибко включать или выключать всевозможные оптимизации и «плагины». Например, если ваш код будет работать только в современных мобильных устройствах, то зачем вам все этих хаки для IE6?! В LMD эту «оптимизацию» можно легко отключить в конфиге. Или вы не желаете загружать CSS динамически — зачем вам таскать лишний код?! — отключаем опцию и код становится меньше. Минимальный объем LMD.js — всего 288 байт.
5. Горячая сборка проекта
Бывают такие веб-приложения которые необходимо каждый раз пересобирать, чтобы проверить, что же получилось. LMD тоже страдает этим, но она не заставляет вас каждый раз выполнять make
или настраивать хитрую сборку на сервере. Вам достаточно запустить LMD в режиме watch и при каждом изменении какого-либо файла, входящего в сборку, LMD пересоберет весь проект.
$ lmd watch config.lmd.json output.js
6. Умный сборщик
Я стараюсь сделать из LMD.js умный сборщик, который сможет во время сборки указать на возможные ошибки — ParseError, отсутствующий/лишний флаг в конфиге, прямое использование глобалов в lazy-модулях и прочие оптимизации.
Столько всяких плюсов, а что же по факту:
По сравнению с AMD мы уменьшили объем еще на 13.5 Кб время старта на 2.1с, а количество запросов на 2.
Если сравнивать с самым первым случаем, то мы получим потрясающие результаты:
Заключение
1. Используйте AppCache
AppCache — это достаточно простая оптимизация, которая, позволит без изменений кода проекта добавить хороший каш вашему приложению. Если вам лень собирать список ваших ресурсов или писать какие-то скрипты вы можете воспользоваться тулзой Confess tinyurl.com/confessjs которая все это сделает за вас и в придачу произведет профилирование ваших CSS селекторов.
2. Соберите скрипты
Это самая выгодная по затратам оптимизация, если вы еще не сжимаете скрипты — перестаньте читать и напишите наконец make файл :)
3. Начните использовать LMD или AMD
Существующие приложения достаточно сложно перевести на LMD или AMD, но если вы начинаете писать, то сделать это просто и выгодно как для ваших пользователей так и для разработчиков. Кроме ленивой загрузки вы получаете еще и полностью изолированные модули, что очень выгодно в командной работе.
Всякие ссылки
Приложение, которое мы оптимизировали azproduction.github.com/loader-test/
Тулзы и скрипты
LMD github.com/azproduction/lmd
Confess tinyurl.com/confessjs
Require.js requirejs.org/
YUI compressor tinyurl.com/yui-compressor
Can I Use caniuse.com/
script-cover code.google.com/p/script-cover/
UglifyJS github.com/mishoo/UglifyJS
Сlosure Сompiler code.google.com/p/closure-compiler/
По AppCache
Статья о AppCache на MDN tinyurl.com/mdn-appcache
FAQ по AppCache appcachefacts.info/
AppCache для новичков www.html5rocks.com/en/tutorials/appcache/beginner/
PS Это дополненная версия моего доклада на DUMP 2012
Автор: azproduction