Пользователь
0,0
рейтинг
7 января 2012 в 15:14

Разработка → Параллельная загрузка JavaScript и CSS без блокирования парсинга страницы

Известно, что следуя идеям старой школы, а именно, добавляя ссылки на JS и CSS в страницы, может обернуться большим временем загрузки страницы. Браузер отображает страницу по мере скачивания, но останавливается, если натыкается на тег script со ссылкой, до того момента, пока скрипт не будет загружен и выполнен. Сайты стали использовать всё большее количество скриптов, начальное отображение страницы занимает всё больше времени, к примеру, на этой странице, которую вы читаете, 13 скриптов, 7 из которых находятся в head'е. Ко всему прочему, некоторые браузеры по-прежнему придерживаются ограничений на одновременное количество загрузок с одного хоста.

Сразу предлагаю принять, что все JS файлы минимизированы, и передаются в сжатом виде.

Существует несколько решений, как то:
— поместить стили и скрипты прямо в страницу;
— установка аттрибутов async/defer тегу script;
— склеить все скрипты в один файл;
— помесить ссылки на скрипты в конец body;
— разместить все файлы на CDN/на разных хостах;
— свой вариант…

Эти решения работают, каждое лучше или хуже в зависимости от того, как построен сам сайт, но обладают рядом недостатков, которые я опишу ниже.
Существует интересная техника, которая решает проблему паузы перед начальным отображением страницы, а заодно добавляет некоторые удобства. Рискну предположить, что техника эта многим знакома, но тем не менее здесь я о ней упоминаний не видел.

Началось всё, конечно, с того, что я взялся за один проект, и в какой-то момент мне показалось, что простенькая страница достаточно долго загружается, и посмотрел на график загрузки, и на результаты YSlow. Огонь на секунду потух в моих глазах, но зная, что может быть лучше, я полез искать,

Разберём недостатки перечисленных выше методик.

Помещение скриптов и стилей прямо в страницу

Очевидно, что это решение подходит только для маленьких страниц, где не много скриптов стилей, так как в случае перезагрузки страницы будут повторно загружаться статические данные, и кеширование не может сработать. Решение вполне оправдано для небольших скриптов и стилей, жизненно необходимых с того самого момента, как страница загружена.

Установка аттрибутов async/defer тегу script

Спецификация тут.
Аттрибуты async и defer тега script поддерживается следующими бразуерами, что может показаться недостаточным для тех, кто делает сайты, на которые могут заходить люди с устаревшими браузерами, а также Opera, что особенно актуально в рунете.
Из недостатков также можно отметить, что скрипты, загруженные из тега с аттрибутом defer, не могут использовать document.write, так как их исполнение не синохронизировано с парсером страницы.

Склеивание скриптов и стилей

Есть интересная заметка (осторожно, англ.) и тест, которые показывают, что загрузка одного большого файла может длиться дольше, чем параллельная загрузка отдельных файлов. Однако, эта техника не только достойна упоминания и жизни, но и также крайне показана к применению в том случае, если разные страницы сайта используют один и тот же набор скриптов или стилей.

Помещение stylesheet в head, а script — в конец body

Достойно упоминания и использования, но в этом случае, как и в описанных выше, до момента document.ready могут быть неразрешённые зависимости между скриптами, и если для jQuery с плагинами это допустимо, то для варианта, когда мы загружаем библиотеку API Facebook'а или VKontakte, и хотим тут же запустить наш скрипт, который пошёт на API запрос определения, залогинен ли пользователь, это сулит костылями, либо загрузкой библиотеки API в начале страницы, блокируя её отображение, что отягощается необходимостью вызова DNS resolve.

Загрузка с CDN

Использование CDN может снизить загрузку на собственный сервер, но может и увеличить и уменьшить время загрузки, а может и полностью блокировать загрузку скрипта, как в случае атаки на CDN сервер или блокировки CDN сервера на провайдере или в сети предприятия.

Какие ещё есть решения?

Что если попробовать загружать скрипты в тот момент, пока страница грузится, но не выполнять их, и вообще скрывать от браузера, что это скрипты до того момента, пока они не догрузились, чтобы не блокировать первичное отображение страницы?

Где-то в заметках у меня нашёлся HeadJS, но с тех пор, как я впервые на него наткнулся, он серьёзно заматерел, и научился делать не только то, что нам нужно, но и многое другое. Несмотря на то, что библиотека явно хороша, а в минифицированном виде занимает всего 3КБ, я решил поискать альтернатив и нашёл аж 14 аналогичных библиотек, краткое и не всегда верное сравнение которых можно найти в этой заметке, плюс load.js и include.js. Бегло пробежавшись по представленным библиотекам и отметя сначала большие (>3КБ), а потом те, которые не понравились мне по синтаксису или принципу работы, я лично для себя выбрал YepNope.js, входящий в состав Modernizr. Авторы библиотеки сообщают, что библиотека не лучше и не хуже остальных, и выполняет ту же задачу, что и остальные, и что они сами в разных проектах используют также и другие загрузчики.

Итак, что же и как делает загрузчик ресурсов на примере YepNope:
<script src='/javascripts/yepnope-min.js'>
  yepnope('/javascripts/jquery.min.js', '/javascripts/jquery.loadmask.min.js', '/javascripts/jquery.jgrowl_minimized.js'])
</script>

Исполнение загруженных скриптов идёт в указанном порядке.

Далее в блоке инициализации:
yepnope({
  load: ['//connect.facebook.net/ru_RU/all.js', '/javascripts/facebook_auth_callback.js'],
  complete: function(){
    FB.init({appId: '273684999999999', xfbml: true, cookie: true, oauth: true})
    FB.Event.subscribe('auth.statusChange', facebook_auth)
  }
})

Итак, мы получаем все желаемые плюшки, как то параллельная загрузка, определённость времени выполнения скрипта, зависиммости, обратные вызовы по готовности.

Пример загрузки стилей:
yepnope(['/stylesheets/jquery.loadmask.css', '/stylesheets/jquery.jgrowl.css'])

У YepNope и других загрузчиков есть также много дополнительных возможностей, описание которых в этом топике не уместно, но с которыми неплохо познакомиться, и каждому выбрать что-то для себя самому.

Авторы скриптов не могут прийти к единому принципу работы и интерфейсу API, и продолжают создавать всё новые загрузч ики. В связи с этим автор HeadJS предлагает встроить поддержку порядка загрузки в спецификации, и автор $script.js его в этом поддерживает, но пока это пройдёт через спецификации и будет работать одинаково во всех браузерах, нам предстоит пользоваться загрузчиками.

Каков же итоговый рецепт?
— встроить в head страницы script, указывающий на загрузчик;
— встроить inline скрипт, использующий загрузчик для подгрузки других скриптов и стилей;
— объединять скрипты и стили, использующиеся только совместно, в один для минимизации количества HTTP запросов;
— минимизировать скрипты и стили;
— убедиться в том, что сервер пакует передаваемые данные gzip'ом;
— убедиться в том, что сервер правильно кеширует;
— осторожно и вдумчиво использовать сторонние CDN и дополнительные хосты.

При написании топика делал оглядку на следующие материалы, рекоммендуемые к прочтению:
[1]. Очерёдность событий и синхронизация в JavaScript
[2]. Простая загрузка скрипта при помощи yepnope.js
[3]. Описание yepnope.js
[4]. Описание $script.js

UPD.
от sHinE Ещё одна таблица-сравнение различных загрузчиков, с более полным списком.
от S2nek Русский перевод описания YepNope.
Phil Pirozhkov @philpirj
карма
154,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

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

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

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

  • +4
    И каково увеличение производительности?
    • +6
      странный вопрос. относительно чего? сферической страницы в вакууме?
      • +7
        Я думаю, было бы очень логично протестировать несколько вариантов разных страниц. А как еще по вашему тесты делают?
        • –8
          вы как раз о сферических страницах в вакууме? параллельную загрузку скриптов делают не чтобы какую-то мифическую «производительность» повышать, а чтобы визуально ускорить для пользователя отклик. пользователь просто не будет видеть задержки, которая происходит при загрузке скриптов, как будто их просто нет.
          • +1
            Можно с реальными страницами крупных сервисов тест произвести
            • 0
              Я думаю, вы ни к чему спорите. Тесты почти всегда синтетические
              • –3
                я думаю, вы высасываете необходимость тестов из пальца. нечего там тестировать.
            • +1
              1) посмотрите как-нибудь на досуге, как устроена статика на фейсбуке. потом на гугле. удачного тестирования.

              2) конкретный коэффициент ускорения зависит количества скриптов, их размера, их взаимозависимостей, группировки, и в конце концов, канала пользователя. замерьте две страницы, или двадцать две, это не будет иметь никакого значения. каждая страница будет иметь свой показатель, вы увидите закономерности только на очень похожих страницах.
              • +2
                Тестирование (проверка теории на практике) — обязательное условие жизнеспособности решения.
                Мало того, предлагаются к рассмотрению различные варианты организации одной и той же страницы! Одной и той же, улавливаете? Сравнить тут разницу сам бог велел. Ну и еще он был бы просто рад, сравнительной диаграмме на различных примерах страниц.
                • 0
                  как я люблю хабр за то, что люди здесь выучивают кучу умных предложений и не вникают в их смысл.

                  не забудьте перед этим сравнить производительность страницы со скриптами в ней и вынесенными в отдельные файлы. потом потестируйте разницу между стилями в отдельных файлах, и прописанных прямо в тегах. а потом ещё круглый камень в сопку потолкайте.
      • +1
        Относительного этого:

        Существует несколько решений, как то:
        — поместить стили и скрипты прямо в страницу;
        — установка аттрибутов async/defer тегу script;
        — склеить все скрипты в один файл;
        — помесить ссылки на скрипты в конец body;
        — разместить все файлы на CDN/на разных хостах;
        • –3
          см. комментарий выше. это всё ускоряет отклик страницы, а не производительность. а фактический коэффициент ускорения отклика уже будет зависеть от канала пользователя. это ускорение не надо мерить. надо просто чтобы для него было сделано всё возможное.
      • +2
        Хотя бы относительно страницы, которую тстировал автор, при виде которой «огонь потух в глазах»
  • –4
    [irony]как много информации-то о yepnope[/irony]

    1.6кб для того, чтобы сделать <script>document.write('<script src=«app.js»></script>')</script>?
    и да, я понимаю, что бывают случаи сложнее. но кода там максимум строк на 40 будет.
    • 0
      На хабре не так давно был перевод статьи о неблокируемой загрузке данных в страницу: там все не так просто. Я уже точно не помню, но, например, некоторые браузеры, не позволяют вести параллельная загрузку более чем, скажем, 5ти скриптов. Следовательно загрузчик должен уметь это решать.
  • +10
    Все время вспоминаю эту табличку — spreadsheets.google.com/lv?key=tDdcrv9wNQRCNCRCflWxhYQ, когда читаю про загрузку скриптов.
    • 0
      Спасибо за ссылку. Просто отлично.
  • +8
    Я ещё раскидываю скрипты, стили, картинки и т.д. по разным поддоменам, чтобы увеличить количество одновременных соединений.
    • +5
      Не знаю почему минусуют, но этот приём даёт очень заметный эффект, к примеру, при загрузке карт, состоящих из десятков или сотен фрагментов.
    • 0
      Это есть в топике (загрузка с разных доменов), почёрпнуто из известного источника.
      • 0
        Этого я не заметил, я заметил только "— разместить все файлы на CDN/на разных хостах;". Суть того, что предлагает B_Vladi — не использование разных хостов, а обман браузера созданием алиасов одному и тому же хосту.
        • 0
          Именно. Тем более это легко реализуется в nginx или .htaccess.
          Моё мнение такое, что всё нужно применять в комплексе и учитывать это с самого начала разработки. Только так будет высокий результат.
          • 0
            С самого начала не обязательно, а вот перед выходом в продуктивный режим и перед ожиданием хабраэффекта это необходимо. С самого начала нужно накидать весь минимальный функционал.
            • 0
              Ну да, это уже тонкости.
  • 0
    Некоторое время использовал $.include.

    Сейчас хватает чуть модифицированного $.getScript:
    $.getScript = function(url, callback, cache){
    	$.ajax({
    		type: 'GET',
    		url: url,
    		success: callback,
    		dataType: 'script',
    		cache: cache || true
    	});
    };

    • 0
      Это здорово, но к тому моменту уже должен быть загружен jQuery, а это ожидание загрузки одного из самых больших скриптов.
      • 0
        Именно.

        Загрузка jQuery → DOMReady → модификация DOM, только в таком порядке.
        • 0
          А если нужно, чтобы к моменту загрузки jQuery уже какие-то данные подгрузились откуда-то, например, оказалось получено имя пользователя из вконтакте, иконка с граватара и список рекомендаций друзей с фейсбука? Зачем ждать загрузки эффектов?
    • 0
      А что будет, если вызвать так? :)

      $.getScript(myUrl, myCallback, FALSE);
      • 0
        Ничего не будет, переменная FALSE, не определена.

        Если же речь идёт о передаче false в качестве параметра, то будет работа как сейчас в jQuery по умолчанию — в адрес будет добавлен get-параметр, для отмены кэширования.
        • +1
          Речь идет о передаче false в качестве параметра. В вашем примере свойство cache будет всегда установлено в true.

          cache: cache || true
          • +1
            Да, действительно, недосмотр.
            Благодарю.
  • +4
    Использую один файл в конце body, а социальное барахло грузится только после загрузки всей страницы после события ready. Результат — страница грузится и отображается мгновенно, а уже потом выполняются скрипты и добавляют функционал. Работает на ура.

    Стили сжимаю в один файл, спрайты создаются автоматом при выкладке в эфир. Работает на ура.
    • 0
      1) Чем делаются спрайты (своя разработка/стороняя)?
      2) Какой алгоритм при принятии решения об объединении картинок в один спрайт (ручное задание/автоматические на основании размеров картинки/автоматическое на основании формата/прочее)?
      3) Генератор спрайтов CSS сам меняет?
      • +2
        1,3: Используется стандартная библиотека css-парсер Tidy, а затем работает разработанный мной алгоритм создания спрайтов, при этом получается css-файл с изменёнными смещениями (входные картинки можно использовать даже спрайты, там простая арифметика =) )

        2: Есть два режима — авто и ручной. Авто — проверка на изменение даты css-файлов. В отдельном каталоге создаются нулевого размера «зеркала» файлов (типа для хранения даты), потом сравниваются.
        • 0
          Не поделитесь инструментом с нами? =)
          • 0
            А Compass'а уже не хватает?
            • 0
              Compass появился недавно, а я разрабатывал инструмент для себя в 2008 году.
  • 0
    Вот здесь тесты: http://headjs.com/#theory

    Исходя из своего опыта скажу, что асинхронная подгрузка скриптов действительно визуально ускоряет загрузку страницы, но:

    Эффект действительно заметен только на старых браузерах и ie. В современных браузерах, при тестировании я нередко получал обратный эффект (правда очень мизерный).

    Если у вас на странице контент, оформление которого зависит от скрипта (чего конечно в идеале нельзя допускать), например слайдер или меню, то при загрузке страница может на мгновение предстать в недооформленном виде.

    $(function(){ }); и всякие подобные «документ реди», вызванные инлайн скриптом на загружаемой странице, перестают работать. Так как скрипты подгружаются асинхронно со страницей, то страница может загрузиться до полной загрузки всех скриптов и некоторые функции и переменные могут быть ещё не определены, что взовет ошибку. Надо вызов кода помещать в дополнительную колбек функцию. Например, в случае head.js:


    $(function(){ 
        head.ready(
            function() { 
                /*code*/ 
        }) 
    });  
    


    • 0
      Простите, а $ откуда? Похоже, что jQuery грузится не HeadJS'ом, и это печально.
      Логичнее бы было так:
      head.js("jquery.js");
      head.ready("jquery.js", function() {
        // и $(function(){}) уже не нужен, этот код будет вызван, когда готова страница И jquery
      });
      
      • 0
        Да, правильно
      • 0
        Возможно я ошибаюсь, но если «jquery.js» загрузится до события «DOM ready» возможна ошибка.
        Думаю, всё же $(function(){}); стоит оставить.
        • 0
          Какого рода ошибка?
          Метод, переданный параметром к head.ready будет исполнен только после наступления dom ready. Двойная проверка не нужна. А вот в случае, если инлайновый скрипт загрузится и выполнится до того, как будет загружен jQuery, и соответственно определён $, будет ошибка, говорящая о том, что $ не определён. И единственный способ её избежать — поставить script src=«jquery.js» в head, что задержит начальное отображение страницы на время, необходимое для загрузки и исполнения jquery.js, с чем мы в этом топике как раз и пытаемся бороться.
          head.ready делает ровно то же, что и $() — выполняет переданную ему функцию как только выстрелит dom ready. вкладывать одну проверку в другую — смысла нет. Равно как и ждать загрузки для этой цели 50КБ+ скрипта вместо ~3КБ (для head.js и т.п.).
          • 0
            Если метод, переданный параметром к head.ready будет исполнен только после наступления dom ready, то вы конечно правы (двойная проверка будет излишней).

            • +2
              Да, всё же насчёт head.js есть тонкость, исходя из документации, а именно:

              head.ready("jquery.js", function() {
                // вызывается, когда загружен и исполнен jquery.js (DOM может быть и не готов) - тут внутри уже имеет смысл использовать $(function(){...}), но никак не наоборот
              });
              
              head.ready(function() {
                // вызывается, когда готов DOM и загружены и исполнены ВСЕ скрипты, тут безопасно вызывать любые методы $, но может оказаться, что можно было безболезненно и раньше, до загрузки Google Analytics, например
              });

              И самый приятный и гибкий вариант:

              head.ready(document, function() {
                // вызывается, когда DOM готов
                // имеет смысл поместить сюда вложенный обработчик события загрузки jQuery:
                head.ready("jquery.js", function() {
                  // вызывается, когда загружен и исполнен jquery.js (и DOM к тому моменту уже тоже готов)
                });
              });

  • +1
    Наверное, следует упомянуть о баге в yepnope, из-за которого «загрузка» скриптов может существенно замедлится в случае, если вы используете complete callback (Замедлится не загрузка скрипта, а время отклика — complete всё время будет вызываться с задержкой 10с). Это справедливо для всех современных браузеров кроме IE9.

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