Пользователь
0,0
рейтинг
5 июня 2012 в 17:21

Разработка → Загрузка и инициализация JavaScript


С появлением мобильного веба наш интернет стал снова плохим, а устройства медленными. 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 и проверить стутус кэша.
if (window.applicationCache) {
    applicationCache.addEventListener('updateready', function() {
        if (confirm('An update is available. Reload now?')) {
            window.location.reload();
        }
    });

    // Или проверить его статус при старте
    if(window.applicationCache.status === window.applicationCache.UPDATEREADY) {  
        if (confirm('An update is available. Reload now?')) {
            window.location.reload();
        }
    }  
}

Плюсы AppCache
1. Надежное кэширование
2. Работа оффлайн
3. Простое управление версиями
4. Своевременное обновление

Минусы AppCache
1. Может закончиться дисковая квота
2. Пользователь может не разрешить вашему сайту использовать кэш (в случае с Firefox)
3. На закэшированной странице не будут доступны онлайн ресурсы, если не прописать NETWORK:\n*
4. Если вы вдруг поставили Expires +300 лет на ваш .appcache файл — ваш пользователь навсегда застрянет на старой версии
5. Редиректы на другие домены воспринимаются как Failure
Еще несколько минусов AppCache www.alistapart.com/articles/application-cache-is-a-douchebag

Позволю себе вставить несколько замечаний от VitaZheltyakov, спасибо ему:
— Обновление файлов в Application Cache при настроенном стандартном кэшировании в FF. — файлы не обновятся.
— Занесите в разделы NETWORK и FALLBACK конкретные ссылки (не патерны) — все не указанные файлы не будут загружаться вообще.
— Во фрейме откройте страницу с Application Cache кэширующим первую — приложение зависнет, циклически загружая страницы.
— Попробуйте работать с оффлайн копией основной страницы, где объявлен манифест — она загрузиться из кэша, как рабочая.


Думаю предельно очевидно какие результаты мы получим при повторной загрузке приложения из кэша — 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 — это достаточно простая оптимизация, которая, позволит без изменений кода проекта добавить хороший кэш вашему веб-приложению. AppCache несет ряд проблем, о которых вы можете прочитать тут Application Cache is a Douchebag. Если вам лень собирать список ваших ресурсов или писать какие-то скрипты вы можете воспользоваться тулзой 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
Статья о проблемах AppCache www.alistapart.com/articles/application-cache-is-a-douchebag

PS Это дополненная версия моего доклада на DUMP 2012

UPD Добавлены проблемы AppCache, спасибо yeremeiev и VitaZheltyakov за полезные ссылки и советы.
Mikhail Davydov @azproduction
карма
449,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (40)

  • +1
    Про AppCache не знал, спасибо. А статистика при его использовании какая?
  • 0
    AppCache — это здорово, главное — не увлекаться=) Как веб-приложение может скачать в кэш 2 ГБ, а вы не заметите
    • 0
      Это уже проблема реализации в конкретном браузере — можно же гибко модерировать квоту, спрашивая пользователя.
  • 0
    Впечатляет, спасибо. У нас проблема не стоит пока, но в будущем очень возможна )

    Один момент, поправьте пожалуйста значек на значок
  • +1
    Еще вариант интеллектуальной загрузки (predictive loading) или фоновой. Загрузка начинается не по требованию, а сразу после загрузки страницы. Пока модуль не загружен все возможные обращения к нему собираются в очередь. После загрузки модуля очередь исполняется и далее вызовы проходят напрямую.
  • 0
    До конца не понял про LMD и require.js. Re.js генерит minified либы для проекта, LMD делает это на лету? Или что?
    • +1
      Сборка
      LMD собирает, сжимает, валидирует, stringify-ет все модули, входящие в проект(прописанные в конфиге) и создает из них 1 пакет/файл в который включает и свой оптимизированный по конфигу код. Все эти упаковки и сжатия гибко настраиваются для каждого модуля. Пакет получается таким, что наружу не видны никакие части приложения (не засоряются глобалы). Также возможны и off-package модули, которые загружаются по требованию.

      Сборка на лету
      При любом изменении файлов, входящих в пакет LMD его необходимо пересобрать. Это можно сделать либо руками/сервером по требованию, либо запустить LMD в режиме watch, что удобнее для разработки.
      • 0
        То есть кроме как сборкой на лету от от require.js + r.js не отличается?
        • +1
          Если бы не отличался, то в нем бы не было смысла.
          1. Ленивая инициализация модулей
          2. Можно не писать лишние обертки define() и тп — модули 1-в-1 Node.js
          3. Максимально гибкая настройка объема lmd.js — десяток(пока) флагов в конфиге, которые включают/выключают определенные возможности — гибко меняют объем кода.
          4. Кэширование модулей в localStorage, которое включается 2-3 опциями.
          {   
              "cache": true
              "cache_async": true,
              "version": "1.6.7-2"
          }

          +index.html c кэшированием
          5. Globalless
          6. «Умный сборщик»

          Тут я подробно описал его особенности github.com/azproduction/lmd
          • 0
            ясно, спасибо, буду копать
  • 0
    AppCache это интересно, недавно изучал его. НО, заметил весьма интересный факт: ни один из сайтов, пишущих про AppCache, его не использует. Более того, я не нашел ни одного его реального использования на просторах интернетов.
    • 0
      Mobile Twitter www.stevesouders.com/blog/2011/09/26/app-cache-localstorage-survey/ (инфа не очень свежая)
    • +3
      Я встречаю периодически запрос на сохранение данных.
      Но у AppCache довольно много других проблем: www.alistapart.com/articles/application-cache-is-a-douchebag/
      • 0
        одна из основных: в AppCache нужно перечислять все ресурсы страницы, иначе при повторном заходе неперечисленные ресурсы (например, картинки) не будут показаны. Таким образом AppCache больше подходит для веб-приложений, но никак не подходит для обычных сайтов / интернет-магазинов и т.д.
        • +1
          NETWORK:
          *
          
          • 0
            да, спасибо. Только после некоторого переосмысления у меня возник основной вопрос: а зачем нужен AppCache с усложненной логикой кэширования, если есть обычный кэш (и два механизма к нему: условное и безусловное кэширование)?
            • 0
              Обычный — общий и поэтому может быть вытеснен любым сайтом: Expires +300years на 20Мб файл — бб кэш.
    • +1
      В Chrome можно посмотреть все сайты, которые сохранили что-то в браузере:
      chrome://appcache-internals/
  • +3
    У этого подхода, безусловно, есть один существенный минус — пользователю по клику на элемент ростера приходится ждать 4 секунды (чтобы загрузились остальные скрипты), что, конечно, не очень хорошо.

    Это тоже можно оптимизировать: когда основной минимум ресурсов загрузился, можно начать асинхронно подгружать и, соответственно, кешировать скрипты, которые с большой вероятностью понадобятся в дальнейшем. Т. е. пока юзер смотрит в ростер и листает его, догружаются недостающие модули, когда он кликнет, с большой вероятностью всё уже будет готово.
  • 0
    288 байтов — это очень странный результат. Advanced оптимизация Google Compiler, которая ломает все обращения к зарезервированным переменным. Проверьте это, плиз.
  • 0
    Почему я бы не рекомендовал использовать LMD — это синхронность. Она хороша для программиста, но очень неприятна пользователю. На время загрузки какого-либо большого модуля, ваша страница полностью зависнет, и перестанет реагировать на все клики и маусмувы пользователя. Это может быть довольно продолжительное время, например во время пика посещаемости, и/или временных снижениях скорости интернета.
    • 0
      А вы пользовались как пользователь и писали как писатель прежде чем советовать?
      Синхронна она при инициализации in-package модулей. Для всех остальных же способов загрузки — как и все другие лоадеры асинхронно(я не псих делать синхронный XHR) Пример такого
      • 0
        Уберу (and synchronous) из ридми, а то это сбивает с толку.
  • 0
    Сочинение модулей в стиле Node.js одобряю.
  • 0
    Не хотелось бы, чтобы у читателей сложилось мнение, что $LAB не помогает в оптимизации страниц. Очевидно, для данного примера это решение бесполезно. Но в случае «обычных» сайтов бывает полезным. Понятно, что AMD более продвинутый способ для загрузки, но он требует системного подхода к созданию рабочих файлов. Для веб-приложения системный подход оправдан, для прочих сайтов — нет, имхо.
  • 0
    Чрезвычайно полезный пост
  • 0
    А если использовать localStorage для серелизаии и хранения объектов, никто не пробовал?
    • 0
      В LMD, который описан в конце этой статьи, кэширование модулей в localStorage прозрачно и включается 2-3 опциями в конфиге
      {   
          "cache": true,       // включаем кэширование индексного файла
          "cache_async": true, // кэширование асинхронных модулей (с сервера)
          "version": "1.6.7-2" // версия для проверки состояния кэша
      }
      
      +кэш-лоадер вместо индексного файла
  • 0
    image
    • 0
      черт возьми. вкладкой ошибся.
  • 0
    А как в lmd отлаживать скомпилированный модуль?
    Например, в require.js можно на локалке подключать модуль без компиляции. В browserify есть ключ --debug (сломался в 2.0, правда).
    Я про то, чтобы в консоли браузера видеть реальные файл / строку.
    • 0
      Можно собрать с SourceMap. Я дебажу без Source Map. LMD генерит предельно debugable код.
      • 0
        О, большое спасибо, разобрался. Немного не очевидна логика работы sourcemap + sourcemap_www + www_root. Ожидал что-то типа sourcemap_url.
        • 0
          Дело в том, что LMD не знает ничего о вашей файловой системе, ни о wwwPath. Поэтому столько писанины, зато отличная переносимость ;-)
  • 0
    Интересно, нет ли чего-то типа Yeoman Generators для генерации базового проекта на LMD? Или чего-то типа `express .`.
    • 0
      Нет, но можно в качестве основы взять github.com/2layer/2layer.github.io проект на Backbone+LMD с хорошим скелетом.
      • 0
        И еще один вопрос: мне нужно писать CJS-модули, которые я буду использовать и в браузере, и в ноде. Я правильно понимаю, что LMD мне хорошо подходит для этой задачи?
        • 0
          Да, подходит. LMD использует CommonJS модули.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.