Статьи

Перевод: Мациевский Николай aka sunnybear
Опубликована: 25 июля 2008

Неблокирующая загрузка JavaScript

Примечание: ниже перевод статьи "Non-blocking JavaScript Downloads", в которой автор рассказывает о проблемах, связанных с подключением JavaScript-файлов, и возможной параллелизации их загрузок. Мои комментарии далее курсивом.

Stoyan StefanovОб авторе: Stoyan Stefanov является веб-разработчиком Yahoo! в группе Exceptional Performance и курирует разработку YSlow — инструмента по измерению производительности. Он также внес значительный вклад в разработку продуктов с открытым кодом, выступал на конференциях и автор технических текстов. Его последняя книга Object-Oriented JavaScript.

Внешние JavaScript-файлы блокируют загрузку страницы и сильно влияют на ее производительность, но существует достаточно простой выход из этой ситуации: использовать динамические теги script и загружать скрипты параллельно, увеличивая тем самым скорость загрузки страницы и улучшая пользовательское восприятие.

Проблема: скрипты блокируют загрузку

Давайте сначала рассмотрим, в чем заключается проблема с загрузкой скриптов. Все заключается в том, что браузер не может сказать, что находится внутри скрипта, не загрузив его полностью. Скрипт может содержать вызовы document.write(), которые изменяют DOM-дерево, или вообще location.href, что отправит пользователя на другую страницу. В последнем случае все компоненты, загруженные на предыдущей странице, могут оказаться ненужными. Чтобы предотвратить загрузки, которые могут оказаться лишними, браузеры сначала загружают, затем анализируют и исполняют каждый скрипт перед тем, как переходить к следующему файлу в очереди на загрузку. В результате каждый вызов скрипта на вашей странице блокирует процесс загрузки и оказывает негативное влияние на скорость загрузки.

Ниже приведена временная диаграмма, которая демонстрирует процесс загрузки медленного JavaScript-файла (сильно преувеличив, можно сказать, что она загружается за 1с). Загрузка скрипта блокирует параллельную загрузку картинок, которые идут сразу за ним:

Временная диаграмма: блокирующее поведение JavaScript-файлов

Рисунок 1. Временная диаграмма: блокирующее поведение JavaScript-файлов

Этот пример демонстрирует описанную ситуацию.

Проблема вторая: число загрузок с одного хоста

На временной диаграмме (кроме блокирования картинок) нам также стоит задуматься о том, что картинки после скрипта загружаются только по две. Это происходит из-за ограничений на число файлов, которые могут быть загружено параллельно. В IE <= 7 и Firefox 2 можно параллельно загружать только 2 файла (согласно HTTP 1.1 спецификации), но и в IE8, и в FF3 это число увеличено до 6.

Можно обойти это, используя несколько доменов для загрузки ваших файлов, потому что ограничение работает только на загрузку двух компонентов с одного хоста. Более подробно эта тема освещена в статье «Исследование производительности, часть 4: максимизация параллельных загрузок в Carpool Lane» от Tenni Theurer.

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

Скрипты внизу странице улучшают пользовательское восприятие

Как говорится в совете, приведенном в способах ускорения загрузки вашего сайта, следует размещать вызовы на внешние файлы скриптов внизу страницы, прямо перед закрывающим тегом </body>. Это, в действительности, не ускорит загрузку страницы (скрипт по-прежнему придется загрузить), однако, поможет сделать отрисовку страницы более быстрой. Пользователи почувствуют, что страница стала быстрее, если увидят какую-то визуальную отдачу в процессе загрузки.

Неблокирующие скрипты

На самом деле, существует простое решение для устранения блокировки загрузки: нам нужно добавлять скрипты динамически, используя DOM-методы. Это как? А нам просто нужно создать новый элемент <script> и прикрепить его к <head>:

var js = document.createElement('script');
js.src = 'myscript.js';
var head = document.getElementsByTagName('head')[0];
head.appendChild(js);

Ниже приведена диаграмма загрузки для нашего тестового случая, только для загрузки скриптов используется описанная технология. Заметим, что третья строка загружается так же долго, но это не влияет на одновременную загрузку других компонентов:

Загрузка неблокирующих скриптов

Рисунок 2. Загрузка неблокирующих скриптов

Тестовый пример

Как мы видим, файлы скриптов уже не блокируют загрузку, и браузер может начать раблотать с другими компонентами. Общее время загрузки при этом сократилось вдвое.

Хочу заметить, что это будет работать только в том случае, если «динамические» скрипты не содержат вызовов document.write. Если это не так, то все такие вызовы нужно будет заменить на element.innerHTML, либо отказаться от использования этой техники.

Зависимости

(Еще) одна проблема, которая связана с динамическим подключением скриптов, заключается в разрешение зависимостей. Предположим, что у вас есть 3 скрипта, и для three.js требуется функция из one.js. Как вы гарантируете работоспособность в этом случае?

Хм, наиболее простым способом является объединение всех скриптов в одном файле. Это не только избавит нас самой проблемы, но и улучшит производительность страницы, уменьшив число HTTP-запросов (правило производительности #1). По поводу увеличения производительности можно и поспорить, особенно, в случае кеширования отдельных файлов, но от описанной проблемы это точно избавит. Подробнее об исследованиях по «нарезке» потока.

Если вам все же приходится использовать несколько файлов, то можно добавить на подгружаемый тег обработчик события onload (это будет работать в Firefox) и onreadystatechange (это будет работать в IE). В этой статье показано, как это лучше осуществить. Для достижения полной кроссбраузерности можно сделать кое-что другое: просто добавить в конец каждого скрипта переменную, которая будет сигнализировать: «Я готов». В качестве переменной можно использовать и массив, который будет содержать маркеры для каждого скрипта.

Как показали мои собственные исследования на тему, проблемы с зависимостями были отмечены на IE6- и Safari3 (под Windows). Из 10 скриптов, которые загружались параллельно (на самом деле, максимум они загружались по 6 в FF3, это связано с внутренними ограничениями браузера на число одновременных соединений с одним хостом), все 10 срабатывали в случайном порядке, начиная с 3–5, как раз в этих браузерах. В других браузерах (Opera 9.5, FF2, FF3, IE8b) такого поведения отмечено не было.

Используем утилиту YUI Get

Утилита YUI Get позволяет вам просто использовать описанный выше механизм. Например, вам нужно загрузить 3 файла: one.js, two.js и three.js. Для этого вы можете просто написать:

var urls = ['one.js', 'two.js', 'three.js'];
YAHOO.util.Get.script(urls);

YUI Get также помогает выполнить все зависимости: скрипты не только загружаются в установленном порядке, но и по очереди проходят через функцию обратного вызова onSuccess, когда загрузка завершается. Аналогично вы можете добавить что-то в функцию onFailure, чтобы как-то прореагировать в том случае, если скрипты не загрузятся.

var myHandler = {
    onSuccess: function(){
        alert(':))');
    },
    onFailure: function(){
        alert(':((');
    }
};

var urls = ['1.js', '2.js', '3.js'];
YAHOO.util.Get.script(urls, myHandler);

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

Вы также можете динамически загружать таблицы стилей при помощи YUI Get, используя метод YAHOO.util.Get.css() [пример].

И все это приводит к следующему вопросу:

А что насчет таблиц стилей?

Таблицы стилей не блокируют загрузку дальнейших компонентов в IE, но делают это в Firefox. Применение техники динамического добавления тегов позволит решить проблему. Вы можете динамически создать теги link примерно следующим образом:

var h = document.getElementsByTagName('head')[0];
var link = document.createElement('link');
link.href = 'mycss.css';
link.type = 'text/css';
link.rel = 'stylesheet';
h.appendChild(link);

Это существенно ускорит загрузку страницы в Firefox, однако, не повлияет на IE.

Имхо, тут уже автора заносит. Наиболее простым путем по ускорению первичного отображения страницы будет следование двум простым правилам:

  • Объединить все таблицы стилей в одном файле (через @media)
  • Выставить ссылку на таблицу стилей максимально близко к началу head (это позволит браузеру начать загрузку именно стилей до обращения к favicon или скриптам, которые будут в head).

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

Другим положительным эффектом от динамической загрузки таблиц стилей (в FF) может стать то, что они способствуют «прогрессивному» отображению. Обычно браузеры отображают белый экран, пока не получат все таблицы стилей до последнего байта, только после этого начинается отображение страницы. Это поведение помогает сэкономить ресурсы на возможную перерисовку при поступлении новых стилевых правил. С применением динамических <link> этого не происходит в Firefox: отображение страницы будет происходить без ожидания оставшихся стилей. Как только браузер получит новые стили, он их применит. IE будет вести себя по-прежнему и ждать всех стилей.

Но перед тем, как ринуться внедрять эту технику на ваших страницах, стоит понять одну простую вещь: динамические link нарушают правило разделения форматирования (CSS) и поведения (JS) страницы. Также возможно, что такое поведение браузера будет исправлено в будущих версиях Firefox.

А если по-другому?

Ниже приведено сравнение других методов для снятия блокировки с загрузки скриптов, но все они также обладают своими недостатками.

МетодНедостатки
Используем атрибут defer тега scriptРаботает только в IE, и все
Используем document.write() для подключения тега script
  1. Неблокирующее поведение возможно только в IE (через defer)
  2. Не рекомендуется широко использовать document.write
Используем XMLHttpRequest для получения тела скрипта, затем его исполняем через eval()
  1. «eval() — зло »
  2. Возможно использование только с того же домена
Используем XHR-запрос для получения тела скрипта, затем создаем новый тег скрипт и устанавливаем его содержание
  1. Еще сложнее, чем предыдущий случай
  2. Такое же ограничение на домен
Загрузка скрипта в iframe
  1. Сложно
  2. Издержки на создание iframe
  3. Такое же ограничение на домен

В будущем

В Safari и IE8 уже внесены изменения, которые коснулись способа загрузки скриптов. Идея заключается в том, чтобы загружать скрипты параллельно, но исполнять в той последовательности, в которой они находятся на странице. По всей видимости, в один прекрасный день проблема блокирующих скриптов при загрузке станет попросту неактуальной, потому что будет касаться только пользователей IE7- или FF3-. Пока же наиболее простым способом решения данной проблемы является использование динамического тега script.

Заключение

  • Скрипты блокируют загрузку компонентов в браузерах FF и IE и замедляют загрузку страницы (по моим данным, такая блокировка присутствует во всех браузерах).
  • Наиболее простым решением является использование «динамических» тегов <script> для предотвращения блокировки.
  • Утилита YUI Get позволяет легко использовать описанный выше механизм для добавления стилей и скриптов и отслеживания зависимостей.
  • Вы также можете использовать «динамические» теги <link>, но стоит подумать о разделении слоев представления HTML-документа.

Читать дальше

Все комментарии (habrahabr.ru)