Кроссбраузерный window.onload

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

Событие window.onload используется программистами для старта их веб-приложения. Это может быть что-то довольно простое, например, выпадающее меню, а может быть и совсем сложное – скажем, запуск почтового приложения. Суть проблемы заключается в том, что событие onload срабатывает только после того, как загрузится вся страница (включая все картинки и другое бинарное содержимое). Если на странице много картинок, то можно заметить значительную задержку между загрузкой страницы и тем моментом, когда она начнет фактически работать. На самом деле, нам нужен только способ определить, когда DOM полностью загрузится, чтобы не ждать еще и загрузку картинок и других элементов оформления.

Firefox впереди планеты всей

В Firefox есть событие специально для этих целей: DOMContentLoaded. Следующий образец кода выполняет как раз то, что нам нужно, в Mozilla-подобных браузерах (а также в Opera 9 и старше):

// для Firefox
if (document.addEventListener) {
document.addEventListener(“DOMContentLoaded”, init, false);
}

А Internet Explorer?

IE поддерживает замечательный атрибут для тега <script>: defer. Присутствие этого атрибута указывает IE, что загрузку скрипта нужно отложить до тех пор, пока не загрузится DOM. Однако это работает только для внешних скриптов. Следует также заметить, что этот атрибут нельзя выставлять, используя другой скрипт. Это означает, что нельзя создать <script> с этим атрибутом, используя DOM-методы, – атрибут будет просто проигнорирован.

Используя этот удобный атрибут, можно создать мини-скрипт, который и будет вызывать наш обработчик onload:

<script defer src=”ie_onload.js” type=”text/javascript”></script>

Содержание этого внешнего скрипта будет состоять только из одной строчки кода:

init();

Условные комментарии

Есть некоторая проблема с этим подходом. Другие браузеры проигнорируют атрибут defer и загрузят этот скрипт сразу же. Существует несколько способов, как можно с этим побороться. Можно воспользоваться условными комментариями, чтобы скрыть «отложенный» скрипт:

<!–[if IE]><script defer=”defer” src=”ie_onload.js”></script><![endif]–>

IE также поддерживает условную компиляцию. Следующий код будет JavaScript- эквивалентом для заявленного выше HTML-кода:

// для Internet Explorer
/*@cc_on @*/
/*@if (@_win32)
document.write(“<script defer=\”defer\”
src=\”ie_onload.js\”><\/script>”);
/*@end @*/

Все так просто?

И, конечно же, нам нужно обеспечить поддержку для остальных браузеров. У нас есть только один выход – стандартное событие window.onload:

// для остальных браузеров
window.onload = init;

Двойное выполнение

Остается одна маленькая неприятность (кто сказал, что будет легко?). Поскольку мы устанавливаем событие onload для всех (оставшихся) браузеров, то init сработает дважды в IE и Firefox. Чтобы это обойти, нам нужно сообщить функции, что она должна выполняться только один раз. Итак, наш метод init будет выглядеть примерно так:

function init() {
// выходим, если функция уже выполнялась
if (arguments.callee.done) return;
// устанавливаем флаг, чтобы функция не исполнялась дважды arguments.callee.done = true;
// что-нибудь делаем
};

Стоит также рассмотреть выставление глобальной переменной READY в данном случае. Ведь иногда знать, что страница начала функционировать, может потребоваться не только одной функции init.

Избавляемся от внешнего файла

У описанного решения существует пара минусов:

  • Для IE нам требуется внешний JavaScript-файл.
  • Не поддерживается Safari (Opera 9 поддерживает DOMContentLoaded).

Однако есть решение и для Internet Explorer, которое не зависит от внешних файлов (к сожалениию, на данный момент вызывает предупреждение безопасности в IE7 при использовании защищенного соединения):

// для Internet Explorer (используем условную компиляцию)
/*@cc_on @*/
/*@if (@_win32)
document.write(“<script id=\”__ie_onload\” defer=\”defer\”
src=\”javascript:void(0)\”>
<\/script>”);
var script = document.getElementById(“__ie_onload”);
script.onreadystatechange = function() {
if (this.readyState == “complete”) {
init(); // вызываем обработчик для onload } };
/*@end @*/

И для Safari!

if (/WebKit/i.test(navigator.userAgent)) { // условие для Safari
var _timer = setInterval(function() {
if (/loaded|complete/.test(document.readyState)) {
clearInterval(_timer);
init(); // вызываем обработчик для onload
} }, 10);
}

Полное решение

function init() {
// выходим, если функция уже выполнялась
if (arguments.callee.done) return;
// устанавливаем флаг, чтобы функция не исполнялась дважды
arguments.callee.done = true;
// что-нибудь делаем
};

/* для Mozilla/Firefox/Opera 9 */
if (document.addEventListener) {
document.addEventListener(“DOMContentLoaded”, init, false);
}

/* для Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
document.write(“<script id=\”__ie_onload\”
мdefer=\”defer\” src=\”javascript:void(0)\”>
<\/script>”); var script = document.getElementById(“__ie_onload”);
script.onreadystatechange = function() {
if (this.readyState == “complete”) {
init(); // вызываем обработчик для onload
}
};
/*@end @*/

/* для Safari */
if (/WebKit/i.test(navigator.userAgent)) {  // условие для Safari
var _timer = setInterval(function() {
if (/loaded|complete/.test(document.readyState)) {
clearInterval(_timer);
init(); // вызываем обработчик для onload
}
}, 10);
}

/* для остальных браузеров */
window.onload = init;

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

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

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

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

SpeedUpYourWebsite.v1.2_img_33

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

Число загрузок с одного хоста

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

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

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

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

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

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

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

SpeedUpYourWebsite.v1.2_img_34

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

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

Зависимости

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

Наиболее простым способом является объединение всех скриптов в одном файле. Это не только избавит нас от самой проблемы, но и улучшит производительность страницы, уменьшив число HTTP-запросов. Увеличения производительности может и не произойти в случае кэширования отдельных файлов и повторного посещения, но от описанной проблемы это точно вылечит.

Если все же приходится использовать несколько файлов, то можно добавить на подгружаемый тег обработчик события onload (это будет работать в Firefox) и onreadystatechange (это будет работать в IE). Наиболее кроссбраузерным подходом будет использование меток внутри каждого файла (например, {filename}_loaded) и проверка их с некоторым интервалом на соответствие true. Это обеспечит выполнение зависимостей.

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

Вопрос о динамической загрузке таблиц стилей подробно рассмотрен в четвертой главе (где речь шла о технике объединения картинок и последовательном отображении страницы в случае большого размера CSS-файлов). В случае комбинированной загрузки компоненты надо расположить в порядке приоритетности, потому что браузеры не смогут загрузить все сразу. А пользователи могут попытаться поработать с некоторыми «отложенными» возможностями максимально быстро, что и нужно предвосхитить.

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

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

 

Метод

 

 

Недостатки

 

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

Таблица 12. Сравнение методов «отложенной» загрузки JavaScript-файлов

В будущем

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

Материалы близкой тематики:

Posted in Разгони свой сайт.