Параллельная загрузка 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.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 49
    • +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
                      Ага, это очень ускорит загрузку на медленном одноканальном мобильном соединении =)
                    • 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.

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