Динамически-создаваемые «асинхронные скрипты» являются вредными

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

<!-- ПЛОХО: блокировка внешним сценарием -->
<script src="http://somehost.com/awesome-widget.js"></script>

<!-- ХОРОШО: удаленный сценарий загружается асинхронно -->
<script>
    var script = document.createElement('script');
    script.src = "http://somehost.com/awesome-widget.js";
    document.getElementsByTagName('head')[0].appendChild(script);
</script>

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

Так, это же здорово, верно? Динамически-создаваемые скрипты — это вещь! Не так быстро.

Встраиваемый JavaScript имеет небольшой, но важный (и часто упускаемый из виду) подводный камень: CSSOM блокирует его перед выполнением. Почему? Браузер не знает, что именно такой скрипт планирует сделать, а поскольку JavaScript может манипулировать CSS свойствами, он блокируется и ждет, пока анализируется CSS и строится CSSOM. Лучше один раз увидеть, чем сто раз услышать, рассмотрим следующий пример:

Скриншот

Показанная выше страница подключает CSS файл в верхней части и два «асинхронных сценария» внизу. Иными словами следует всем «лучшим практикам» повышения производительности. За исключением того, что скрипты не могут быть выполнены до тех пор, пока CSSOM не будет готов, что задерживает их выполнение и, следовательно, отправку сетевых запросов. Как следствие, сценарии выполняются через ~3.5 секунды после загрузки страницы.

Обратите внимание, что я специально поставил большие, двухсекундные задержки на отдачу CSS и одну секунду на JavaScript, чтобы подчеркнуть зависимость между CSS/CSSOM и выполнением скриптов.

Теперь давайте сравним это с «плохим» примером, в котором используются два блокирующих тега «script»:

Скриншот

Погодите секундочку, что происходит? Оба сценария будут загружены заранее и выполнены через ~2.7 секунды после загрузки страницы. Обратите внимание, что скрипты будут по прежнему выполняться только после того, как будет доступен CSS (~2.7 секунды), но поскольку скрипты уже загружены в тот момент когда становится доступен CSSOM, мы можем выполнять их сразу, экономя более секунды времени обработки. Мы всё делали неправильно?

Прежде чем мы ответим на этот вопрос, давайте рассмотрим еще один пример, на этот раз с атрибутом «async»:

<script src="http://udacity-crp.herokuapp.com/time.js?rtt=1&a" async></script>
<script src="http://udacity-crp.herokuapp.com/time.js?rtt=1&b" async></script>

Скриншот

При наличии атрибута async скрипт будет выполнен асинхронно, как только он будет доступен. Если атрибут отсутствует, … то скрипт загружается и выполняется немедленно, прежде чем будет продолжен дальнейший анализ документа.

http://www.w3.org/TR/html5/scripting-1.html#attr-script-async

Атрибут async в теге script реализует два важных свойства: он говорит браузеру, чтобы тот не блокировал построение DOM и не блокировал выполнение скриптов перед построением CSSOM. В результате, скрипты выполняются сразу после того, как загрузятся (~1.6 секунды), не дожидаясь CSSOM. Краткий список результатов:

выполнение скрипта событие onload
динамически-создаваемый скрипт ~3.7s ~3.7s
блокирующий скрипт ~2.7s ~2.7s
скрипт с атрибутом `async` ~1.7s ~2.7s

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

  1. async не поддерживается некоторыми старыми браузерами: IE 8/9, Android 2.2/2.3. Эти браузеры игнорируют атрибут и относятся к нему как к блокирующему скрипту. Ранее это было проблемой, что приводит нас к следующему пункту …

  2. Все современные браузеры имеют так называемый «сканер предварительной загрузки» (да, даже IE 8/9 и Android 2.2/2.3), который вызывается в момент блокировки парсера страницы и единственной задачей которого является «заглянуть вперед» и найти ресурсы, которые нужно загрузить, чтобы как можно быстрее расчистить себе путь для рендеринга.

Шаблон script-injected не предоставляет никаких преимуществ по сравнению с <script async>. Он существует потому, что <script async> не был доступен и сканеров предзагрузки не существовало в тот момент, когда этот шаблон впервые был использован. Однако, эта эпоха уже в прошлом, и мы рекомендуем использовать атрибут async вместо скриптов, подключаемых динамически. Короче говоря, динамически-создаваемые скрипты являются вредными.

Так же, обратите внимание, что сканер будет обнаруживать только те ресурсы, которые указываются через src/href атрибуты в тегах скриптов и ссылок. Сканер не может выполнять встроенные блоки с JavaScript, что означает, что любые встраиваемые сценарии не могут быть обнаружены на этапе предварительного анализа. В результате получилась новая рекомендация:

<!-- ПЛОХО: пред асинхронная эра / эра без предварительного анализа страниц -->
<script>
    var script = document.createElement('script');
    script.src = "http://somehost.com/awesome-widget.js";
    document.getElementsByTagName('head')[0].appendChild(script);
</script>

<!-- ХОРОШО: современно, просто, быстро и лучше всего остального  -->
<script src="http://somehost.com/awesome-widget.js" async></script>
<script src="..."> <script async src="...">
Блокирует построение DOM Не блокирует построение DOM
Выполнение блокируется CSSOM Выполнение не блокируется CSSOM
Предварительная загрузка Предварительная загрузка
Упорядоченное выполнение скриптов Выполнение вразнобой
Используйте там, где имеет значение порядок выполнения. Размещать такие скрипты следует в нижней части. Может быть размещен в любом месте, идеальное решение для сценариев, которые могут выполняться в произвольном порядке.

Но подождите, а как же…

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

  1. Атрибут «async» не дает никаких гарантий относительно порядка выполнения: скрипты не выполняются по мере их подключения, их порядок и расположение в документе ни на что не влияет. В результате, если ваши скрипты имеют зависимости, вы сможете их разрешить? В качестве альтернативы, сможете без особых проблем отложить их выполнение или поставить в очередь? Последовательный вызов асинхронных функций — отличная тема для исследования.

  2. Очереди асинхронных функций требуют инициализации некоторых переменных, что подразумевает необходимость использования встраиваемых скриптов. Все вернется на круги своя? Нет, если вы разместите встраиваемый блок выше CSS деклараций — да, вы правильно прочитали, JavaScript перед CSS в <head> — то инлайн-скрипт будет выполнен немедленно. Проблема встраиваемых скриптов в том, что они должны блокироваться CSSOM, но если мы поставим их перед объявлением CSS, то они выполняются сразу.

  3. То есть я просто должен разместить весь мой JavaScript перед CSS? Нет. Вы ведь хотите сохранить чистоту <head>, чтобы позволить браузеру как можно быстрее добраться до CSS и начать разбор контента страницы, то бишь хотите оптимизировать содержание страницы для максимально быстрой отрисовки.