Размышления об асинхронности.

Источник: Thinking Async.
Автор:  Chris Coyier.
Перевод: .

Суть вопроса: Загрузку стороннего JavaScript ресурса следует производить асинхронно. Вам никто не запрещает поступать аналогично с собственными скриптами, но в данной статье давайте сфокусируемся именно на ресурсах, предоставляемых третьей стороной.

На это есть две причины:

  1. Если третья сторона даст сбой или будет недостаточно быстрой, то это не повлияет на загрузку вашей страницы.
  2. Так документ загрузится быстрее.

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

Давайте изучим вопрос асинхронности более подробно.

Итак, с чего начнем?

Для того чтобы понять суть термина «асинхронный», нам придется познакомиться с небольшой, связанной с парсингом терминологией.

«Блокировка парсера» — если в процессе обработки исходного кода вашего документа браузер встречает элемент <script>, то он приступает к полной загрузке данного ресурса и лишь затем продолжает процесс парсинга. Это значительно замедляет загрузку страницы, особенно если скрипт находится в самом начале документа или перед визуальными объектами. Так ведут себя старые браузеры, хотя новые тоже не исключение, и если вы не будете использовать атрибут async, то в большинстве случаев они поступят таким же образом. Вот, что сказано в документации MDN (Mozilla Developer Network): «Если в процессе работы парсера ранних версий браузеров, не поддерживающих атрибут async, встречаются скрипты, то они блокируют его работу…»

С целью предотвращения проблемных ситуаций блокировки парсера, сами скрипты можно делать «внутрискриптовыми» (то есть последующий скрипт вставляется с помощью JavaScript кода). При таком подходе они будут выполняться асинхронно (исключение составляют Opera и Firefox до версии 4.0).

«Блокировка ресурса» — в ходе загрузки скрипта могут блокироваться одновременные загрузки других ресурсов. Именно так ведут себя IE6 и IE7, которые позволяют загружаться единственному скрипту, приостанавливая доставку всех других ресурсов. Немного лучше дело обстоит с IE8 и Safari 4, которые поддерживают параллельную загрузку нескольких скриптов, но при этом блокируют все остальные загрузки (см. ссылку).

В идеале нам нужно решить обе проблемы и тем самым ускорить процесс загрузки страницы (как реально, так и в его восприятии пользователем).

Используем возможности HTML5.

В HTML5 для элемента script предусмотрен специальный атрибут async. Вот пример:

<script async src="https://third-party.com/resource.js"></script>

Проверить поддержку данного функционала браузерами можно вот здесь.

В общем-то, неплохая идея, выполнять загрузку скрипта прямо вот так, используя атрибут async. Это позволяет предотвратить блокировку парсера. Новые версии браузеров будут корректно обрабатывать подобный код и проблем с ними быть не должно, но парсер — это штука серьезная. В Wufoo мы такой прием не используем, поскольку нам нужна более глубокая и надежная кроссбраузерность.

Классический способ асинхронной загрузки.

Ниже представлен классический шаблон загрузочного скрипта, обеспечивающего вас самой глубокой поддержкой браузеров, о которой вы можете только мечтать:

<script>
var resource = document.createElement('script');
resource.src = "https://third-party.com/resource.js";
var script = document.getElementsByTagName('script')[0];
script.parentNode.insertBefore(resource, script);
</script>

Вот более эффективная версия кода, представленная с помощью универсальной функции-обертки (спасибо Матиасу Биненсу / Mathias Bynens):

(function(d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src = 'https://third-party.com/resource.js';
s.parentNode.insertBefore(g, s);
}(document, 'script'));
Но, имейте в виду, что использовать этот способ уже не рекомендуется! Времена, когда такой шаблон считался полезным, давно уже прошли. Поэтому сейчас необходимо отдавать предпочтение атрибуту async.

Рекламные сети.

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

<script type="text/javascript">
(function(){
var bsa = document.createElement('script');
bsa.type = 'text/javascript';
bsa.async = true;
bsa.src = 'https://s3.buysellads.com/ac/bsa.js';
(document.getElementsByTagName('head')[0]||document.getElementsByTagName('body')[0]).appendChild(bsa);
})();
</script>

Несколько моментов, на которые следует обратить внимание:

  • Явная предустановка значения true для атрибута async — это верный шаг в отношении исключительно Firefox 3.6, который является единственным браузером, не делающим это самостоятельно по умолчанию. Хотя в большинстве случаев этот момент можно опустить. Тип скрипта type тоже необязательно указывать.
  • Используемый в приведенном ниже несложном примере атрибут src определен с помощью протоколо-зависимого URL. Это чертовски правильный способ загрузки скрипта как по HTTP, так и по HTTPS протоколу, в зависимости от типа запрашиваемой ресурс страницы. (*Если нужный вам ресурс доступен по SSL, тогда при его запросе всегда нужно указывать схему https://. Потому, что в противном случае, делая запрос на объект по протоколу HTTP, вы автоматически открываете дверь атакам типа Man-on-the-side. С точки зрения безопасности будет более разумно использовать HTTPS соединение, даже если ваш сайт работает по HTTP. Хотя в обратной последовательности (HTTP запрос на HTTPS) это не сработает. Подробнее этот вопрос рассмотрен здесь и здесь.).
    Мы с удовольствием использовали бы такой прием в Wufoo, однако, нами, к сожалению, было обнаружено, что он является причиной появления ошибки в IE6 со стандартными настройками безопасности. Поэтому, если поддержка IE6 не входит в ваши планы, то без раздумий используйте этот метод.
  • Данный код добавляет скрипт в элемент head или body, что встретится раньше. В общем, неплохой прием, однако было бы куда безопасней искать элемент script, поскольку вставляемый фрагмент кода тоже является скриптом.

Рекламный сервис The Deck использует другой шаблон:

<script type="text/javascript">
//<![CDATA[
(function(id) {
document.write('<script type="text/javascript" src="' +
'http://connect.decknetwork.net/deck' + id + '_js.php?' +
(new Date().getTime()) + '"></' + 'script>');
})("DF");
//]]>
</script>

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

Что если вам нужен обратный вызов функции?

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

<script src="https://third-party.com/resource.js"></script>
<script>
someFunction('parameters');
</script>

Служба веб-шрифтов Typekit — как раз тот случай. Ее работа подразумевает предварительную загрузку Typekit скрипта и последующий его запуск. Стоит обратить внимание на то, что в случае с Typekit факт блокировки парсера скриптами проходит безболезненно, более того, сервис использует это явление для собственных нужд. И если ваша страница притормаживает по причине доставки их скрипта, то вам не придется наблюдать связанный с этим FOUT эффект (*Flash Of Unstyled Text — «Кратковременное появление не стилизованного текста». Речь идет о промежутке времени, когда оформленный с помощью веб-шрифтов текст отображается с помощью альтернативного, стандартного системного шрифта, поскольку требуемый для корректного представления файл шрифта еще не загружен. Браузер при этом пытается как можно быстрее предоставить пользователю содержимое страницы в ущерб его визуального оформления). Исключением может быть лишь Firefox, а также случаи использования шрифтов самого Typekit, указываемых в рамках блока @font-face, для загрузки которых применяется JavaScript.

<script type="text/javascript" src="https://use.typekit.com/abc1def.js"></script>
<script type="text/javascript">try{Typekit.load();}catch(e){}</script>

Разумное решение, хотя слегка небезопасное. Что, если по какой-либо причине Typekit будет слишком медленным или временно недоступным. Сами разработчики сервиса говорят об этой ситуации следующее: «То, что ранее было желаемой задержкой визуализации, которая использовалась для нейтрализации эффекта FOUT, превращается в серьезную проблему, если время загрузки скрипта превышает несколько секунд». Цитата из блога Typekit.

Вот как можно асинхронно загрузить Typekit ресурс:

<script type="text/javascript">
TypekitConfig = {
kitId: 'abc1def'
};
(function() {
var tk = document.createElement('script');
tk.src = 'https://use.typekit.com/' + TypekitConfig.kitId + '.js';
tk.type = 'text/javascript';
tk.async = 'true';
tk.onload = tk.onreadystatechange = function() {
var rs = this.readyState;
if (rs && rs != 'complete' && rs != 'loaded') return;
try { Typekit.load(TypekitConfig); } catch (e) {}
};
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(tk, s);
})();
</script>

Обратите внимание, сколько потребовалось «сучковатого» кода для подключения функции обратного вызова к событию onload. Но это, к сожалению, единственный способ получения кроссбраузерной работы обратного вызова. Хочу заранее предупредить вас, что использование данного шаблона не гарантирует полное исчезновение проблемы FOUT. И если вы на самом деле хотите добиться асинхронности в случае с Typekit и получить максимальный эффект от использования данного сервиса, то вам нужно прочесть их пост, в котором речь идет о ловких манипуляциях именами классов и событиями процесса загрузки шрифтов.

Загрузчики скриптов jQuery и им подобные.

Если в вашем проекте уже задействован jQuery, то проблема доставки стороннего скрипта и получения обратного вызова по окончании его загрузки отпадает сама собой:

$.ajax({
url: 'https://third-party.com/resource.js',
dataType: 'script',
cache: true, // иначе при каждой загрузке страницы будет получена новая копия скрипта
success: function() {
// скрипт доставлен, выполняются дальнейшие действия
}
}

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

Если вы обеспокоены увеличением количества и объема загрузок и по этой причине не используете никакой вспомогательной библиотеки, то возможным решением для вас будет YepNope — супер легкий загрузчик скриптов. В идеале лучше провести тестирование, после чего окончательно решить, стоит ли вам использовать этот скрипт, который, кстати, удобен еще и тем, что обладает прямыми методами:

yepnope.injectJs("https://third-party.com/resource.js", function () {
// действия, выполняемые по окончании загрузки скрипта
});

Статья в тему: «Асинхронная загрузка Typekit с помощью YepNope», автор Макс Виллер (Max Wheeler).

Можно ли обойтись без обратного вызова?

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

<script type="text/javascript">var host = (("https:" == document.location.protocol) ? "https://secure." : "http://");document.write(unescape("%3Cscript src='" + host + "wufoo.com/scripts/embed/form.js' type='text/javascript'%3E%3C/script%3E"));</script><br />
<script type="text/javascript">
var z7p9p1 = new WufooForm();
z7p9p1.initialize({
'userName':'chriscoyier',
'formHash':'z7p9p1',
'autoResize':true,
'height':'546',
'ssl':true});
z7p9p1.display();
</script>

Присутствующие в скрипте пары ключ/значение дают возможность пользователю увидеть свои данные и при необходимости изменить их или добавить новые. Нами было сделано несколько попыток достижения необходимого результата путем передачи данных при запросе самого скрипта в качестве составной части URL адреса. В этом случае находящийся на нашей стороне сценарий должен быть PHP обработчиком, который способен извлекать из запроса необходимые значения с помощью константы $_GET. Таким образом, мы могли бы обойтись без громоздкого кода обратного вызова, содержащегося в представленном выше асинхронном шаблоне. Что-то вроде этого:

script.src = 'https://wufoo.com/form.js?data=' + JSON.stringify(options);

Но, в итоге мы отказались от этого варианта. На фоне недостаточно глубокой кроссбраузерной поддержки JSON метода stringify код обратного вызова выглядит не таким уж плохим. Следует также отметить, что замена этого метода полифилом является нецелесообразной. Кроме того, глупо пренебрегать возможностью кэширования нашего сценария form.js.

Социальные сети.

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

Facebook
<div id="fb-root"></div>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "https://connect.facebook.net/en_US/all.js#xfbml=1&appId=200103733347528";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>
Twitter
<a href="https://twitter.com/share" class="twitter-share-button">Tweet</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="https://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
Google Plus
<g:plusone annotation="inline"></g:plusone>
<script type="text/javascript">
(function() {
var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
po.src = 'https://apis.google.com/js/plusone.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
})();
</script>
Наведение порядка.

Приведенные выше шаблоны имеют много общего и в тоже время слегка разнятся. Вставка всех этих фрагментов непосредственно в документ в их чистом виде способна расстроить пуристов кода (кем и мы с вами являемся) до слез. Спасти положение может простой и эффективный способ объединения данных шаблонов, предложенный Николасом Галлахером (Nicholas Gallagher):

(function(doc, script) {
var js,
fjs = doc.getElementsByTagName(script)[0],
add = function(url, id) {
if (doc.getElementById(id)) {return;}
js = doc.createElement(script);
js.src = url;
id && (js.id = id);
fjs.parentNode.insertBefore(js, fjs);
};

// Google Analytics
add('https:://ssl.google-analytics.com/ga.js', 'ga');
// Google+ button
add('https://apis.google.com/js/plusone.js');
// Facebook SDK
add('https://connect.facebook.net/en_US/all.js', 'facebook-jssdk');
// Twitter SDK
add('https://platform.twitter.com/widgets.js', 'twitter-wjs');
}(document, 'script'));

В случае использования CMS.

WordPress, как, собственно, и другие основные системы управления контентом чертовски объемные и имеют свои нюансы при их использовании. Это необходимо учитывать, если вы сами являетесь той самой третьей стороной, предоставляющей шаблоны JavaScript кода, которые пользователь получает посредством обычного копипаста. Здесь ключевым моментом является, конечно же, тестирование. А единственное и самое главное правило заключается в том, что необходимо избегать присутствия в коде двойных переносов строки:

<script type="text/javascript">
var s = d.createElement(t), options = {

foo: bar

}
// The line break space above is bad!
</script>

На первый взгляд все вроде бы превосходно, увеличенные интервалы делают код более читабельным и внятным. Однако следует знать, что в случае, например, с WordPress некоторые отдаленные друг от друга фрагменты кода будут выделены в самостоятельные абзацы путем их включения в элемент <p>. Это связано с присущим данной CMS свойством автоматической фильтрации текста, реализуемой с помощью wp-функции wpautop(). Нетрудно догадаться, что такое вмешательство приведет к неработоспособности скрипта.

Окончательный Wufoo сниппет.

И вот на чем мы остановились:

<div id="wufoo-z7w3m7">
Fill out my <a href="http://examples.wufoo.com/forms/z7w3m7">online form</a>.
</div>
<script type="text/javascript">var z7w3m7;(function(d, t) {
var s = d.createElement(t), options = {
'userName':'examples',
'formHash':'z7w3m7',
'autoResize':true,
'height':'260',
'async':true,
'header':'show',
'ssl':true};
s.src = 'https://wufoo.com/scripts/embed/form.js';
s.onload = s.onreadystatechange = function() {
var rs = this.readyState; if (rs) if (rs != 'complete') if (rs != 'loaded') return;
try { z7w3m7 = new WufooForm();z7w3m7.initialize(options);z7w3m7.display(); } catch (e) {}};
var scr = d.getElementsByTagName(t)[0], par = scr.parentNode; par.insertBefore(s, scr);
})(document, 'script');</script>

По правде говоря, мы были обеспокоены размером сниппета. Пятьдесят строк это слишком много для кода с подобным функционалом, но сейчас его объем сокращен до 19 строк. И хотя он превышает размер используемого ранее кода, это вполне допустимо. Большинство строк задействовано под опции объекта, которые можно было разместить компактнее в несколько строк. Однако в целях читабельности и удобства редактирования их все же следует оставить в таком виде — по одной на каждой строке.

В шаблоне не использованы протоколо-зависимые URL, поскольку поддержка IE6 все еще актуальна для нас. Проблема решается с помощью location.protocol теста.

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

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

«Водопады» производительности.

Если вы заинтересованы в проведении детального тестирования процесса загрузки ресурса, то анализ диаграмм обработки документа («водопады производительности») — это то, что нужно. Если говорить о старых версиях IE (6-8), то они не предоставляют подобную информацию, но современные средства веб-разработчика обладают собственными инструментами, позволяющими делать такой анализ. Взять, к примеру, Firebug (вкладка "Net") или ресурс Web Page Test. Внешний вид, конечно, не очень, но зато как информативно!

В ходе тестирования сниппета Wufoo для IE6 я смог доказать, что новый способ загрузки формы предотвращает блокирование, в отличие от предыдущей версии кода:

Ну вот вроде бы и все. В ходе написания этой статьи я чувствовал себя неуверенно, поскольку не являюсь экспертом в этой области. Поэтому ваши коррективы и дополнения приветствуются.

* Примечание переводчика.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *