В моей статье «Понимание критического пути рендеринга» (перевод статьи) я писала о том, какой эффект оказывают JavaScript-файлы на Критический Путь Рендеринга(CRP).
JavaScript является блокирующим ресурсом для парсера. Это означает, что JavaScript блокирует разбор самого HTML-документа. Когда парсер доходит до тега <script>
(не важно внутренний он или внешний), он останавливается, забирает файл (если он внешний) и запускает его.
Такое поведение может доставить проблемы, если мы загружаем несколько JavaScript-файлов на странице, так как это увеличивает время первой отрисовки, даже если документ на самом деле не зависит от этих файлов.
К счастью, элемент <script>
имеет два атрибута async
и defer
, которые дают нам возможность контролировать то, как внешние файлы загружаются и выполняются.
Нормальное выполнение
Прежде чем понять разницу между этими двумя атрибутами, давайте посмотрим что происходит в их отсутствие. Как было сказано ранее, по умолчанию файлы JavaScript прерывают парсинг HTML-документа до тех пор, пока не будут получены и выполнены.
Возьмём пример, в котором элемент <script>
расположен где-то в середине страницы:
<html>
<head> ... </head>
<body>
...
<script src="script.js">
....
</body>
</html>
Вот что произойдёт, когда парсер будет обрабатывать документ:
Парсинг HTML приостанавливается, пока скрипт не будет загружен и выполнен, тем самым увеличивая количество времени до первой отрисовки.
Атрибут async
Async
используется для того, чтобы указать браузеру, что скрипт может быть выполнен асинхронно.
Парсеру HTML нет необходимости останавливаться, когда он достигает тега <script>
для загрузки и выполнении. Выполнение может произойти после того, как скрипт будет получен параллельно с разбором документа.
<script async src="script.js">
Атрибут доступен только для файлов, подключающихся внешне. Если внешний файл имеет этот атрибут, то он может быть загружен в то время как HTML-документ ещё парсится. Парсер будет приостановлен для выполнения скрипта, как только файл скрипта будет загружен.
Атрибут defer
Атрибут defer
указывает браузеру, что скрипт должен быть выполнен после того, как HTML-документ будет полностью разобран.
<script defer src="script.js">
Как и при асинхронной загрузке скриптов — файл может быть загружен, в то время как HTML-документ парсится. Однако, даже если файл скрипта будет полностью загружен ещё до того, как парсер закончит работу, он не будет выполнен до тех пор, пока парсер не отработает до конца.
Асинхронное, отложенное или нормальное выполнение?
Итак, когда же следует использовать асинхронное, отложенное или нормальное выполнение JavaScript? Как всегда, это зависит от ситуации и существуют несколько вопросов, которые помогут принять вам правильное решение.
Где расположен элемент <script>
?
Асинхронное и отложенное выполнения наиболее важны, когда элемент <script>
не находится в самом конце документа. HTML-документы парсятся по порядку, с открытия <html>
до его закрытия. Если внешний JavaScript-файл размещается непосредственно перед закрывающим тегом </body>
, то использование async
и defer
становится менее уместным, так как парсер к тому времени уже разберёт большую часть документа, и JavaScript-файлы уже не будут оказывать воздействие на него.
Скрипт самодостаточен?
Для файлов, которые не зависят от других файлов и/или не имеют никаких зависимостей, атрибут async
будет наиболее полезен. Поскольку нам не важно, когда файл будет исполнен, асинхронная загрузка — наиболее подходящий вариант.
Полагается ли скрипт на полностью разобранный DOM?
Во многих случаях файл скрипта содержит функции, взаимодействующие с DOM. Или, возможно, существует зависимость от другого файла на странице. В таких случаях DOM должен быть полностью разобран, прежде чем скрипт будет выполнен. Как правило, такой файл помещается в низ страницы, чтобы убедиться, что для его работы всё было разобрано. Однако, в ситуации, когда по каким-либо причинам файл должен быть размещён в другом месте — атрибут defer
может быть полезен.
Скрипт небольшой и зависим?
Наконец, если скрипт является относительно небольшим и/или зависит от других файлов, то, возможно, стоит определить его инлайново. Несмотря на то, что встроенный код блокирует разбор HTML-документа, он не должен сильно помешать, если его размер небольшой. Кроме того, если он зависит от других файлов, может понадобиться незначительная блокировка.
Поддержка и современные браузерные движки
Поддержка атрибутов async
и defer
очень распространена:
Стоит отметить, что поведение этих атрибутов может немного отличаться в разных движках JavaScript. Например, в V8 (используется в Chromium), сделана попытка разобрать все скрипты, независимо от их атрибутов, на отдельном выделенном потоке для выполнения скрипта. Таким образом, «блокирующая парсер» природа JavaScript-файлов должна быть минимизирована по умолчанию.
Комментарии (39)
mtt
13.03.2017 14:46Когда парсер доходит до тега
Является ли блокирующим следующее объявление?
<script type="text/x-template" id="question"> <div> content </div> </script>
Где лучше размещать подобные блоки, в конце страницы или где угодно?mayorovp
13.03.2017 14:55+3Тут нет ни загрузки сетевого ресурса, ни исполнения тяжелого кода, а потому вопрос является ли это объявление блокирующим — смысла не имеет.
maniacscientist
13.03.2017 15:41Старый я стал, не понимаю, зачем всё это? desktop.min.js и mobile.min.js — ответ на все вопросы, во всяком случае множество разных проблем заменяется на одну — юзеры с медленным соединением, для которых 2 мегабайта при каждом обновлении — это трудно. А уж если за 2 мега вылезли или обновления ежедневно — значит что то делаем не так.
mayorovp
13.03.2017 15:47Нет, это не вы старый, это пост устарел :-) Сейчас как раз склеивание всех скриптов в один файл с его минификацией куда более распространено, чем раньше. requirejs, systemjs, webpack — все они обходят рассмотренную тут проблему, делая ее не такой существенной.
Binjo
13.03.2017 15:56Как сказать. Выделение критических ресурсов для более быстрой отрисовки входит в обиход. К примеру, зачем мне ждать загрузки какой-нибудь карты, если я хочу просто ознакомиться с сайтом (например, сайт какой-нибудь компании) и могу до неё никогда и не дойти? Да и на всякие соц. виджеты может быть наплевать.
mayorovp
13.03.2017 16:00Да, но при использовании современных инструментов экосистемы javascript выделение критических ресурсов делается совсем другими методами, имеющими мало общего с атрибутами async и defer.
Binjo
13.03.2017 16:07Хм. Простой пример. Вот есть скрипт гугловый, который подключает карту. Вот есть мой некий
common.js
, в котором, скажем, описан какой-нибудь полифилл. Сайт встречает заголовком, текстом, формой, кучей текста, картинками и лишь в конце картой. Так вот как раз-таки для скрипта карты и пригодитсяdefer
. Гугл, кстати, так и рекомендует подключать у себя в примере.mayorovp
13.03.2017 16:10… и получаем скрипт, который полностью работоспособен только если на тут же самую страницу подключен другой скрипт? Ну, и еще пара десятков сторонних скриптов?
От этого и стараются уйти, в том числе перечисленными мною инструментами.
Binjo
13.03.2017 16:16Мы ускоряем отрисовку, а не ждём пока у нас загрузятся все ресурсы, которые, возможно, нам и не понадобятся. Чанки о том же, по-сути. Можно собрать всё в единый файл и пусть грузится или же разбить на части и грузить первыми только важные ресурсы, а остальные оставить на потом. Как минимум пользователь уже начнёт взаимодействовать с сайтом, а не будет ждать со словами «сайт тормозной». А, может, и вовсе уйдёт.
mayorovp
13.03.2017 16:19Суть одна — но механизмы-то разные! Вы понимаете, что выделение чанков в вебпаке совсем не похоже на простановку атрибутов async или defer скриптам?
Можно прочитать хоть сотню постов про атрибуты async и defer — но когда понадобится разбить пакет на чанки — придется лезть в документацию. И обратное тоже верно.
Binjo
13.03.2017 16:22Не похоже. Мы разделили ресурсы, а затем сказали браузеру какой ресурс критичен, а какой нет.
KhodeN
13.03.2017 17:06Хотелось бы уточнить по поводу сохранения или не сохранения порядка выполнения нескольких скоиптов в случае с async и defer
Например, если есть большой внешний скрипт и небольшой инлайновый сразу после него, то можно ли сделать внешний скрипт deferred?funca
13.03.2017 22:59В общем случае стоит считать, что порядок выполнения скриптов не гарантируется ни для defer ни для async.
tenbits
13.03.2017 17:16Если внешний JavaScript-файл размещается непосредственно перед закрывающим тегом body, то использование async и defer становится менее уместным
Менее уместно, но всё же уместно? Например, если у нас 5 внешних скриптов в конце
body
то безdefer
, они будут загружаться один за другим (так как парсер соответственно поочередно будет переходить от одного тэга script к другому). А вот сdefer
у каждого тэга можно загрузку распараллелить. Я правильно понимаю?Binjo
13.03.2017 17:47+1Смотрите какая штука.
Вот таким образом он забрал файлы.
А вот таким выполнил:
tenbits
13.03.2017 18:01Уточните пожалуйста, из вашего примера, все 3 скрипта с
defer
атрибутом или без?Binjo
13.03.2017 18:06+2Все 3 имеют defer.
kirillaristov
14.03.2017 00:14Но ведь на втором скрине не видно, какой именно скрипт выполняется? Может они так и идут — 1,2,3? Или суть в том, что они отрабатывают за разное время? Тем самым 2 и 3 выполняются раньше.
funca
13.03.2017 23:07defer влияет на порядок выполнения. Скрипты без атрибута будут выполняться строго один за другим, как написано.
При наличии атрибута defer порядок выполнения не определен. Кроме того, в defer скриптах нельзя выполнять document.write().torbasow
14.03.2017 13:41+1Скрипты без атрибута будут выполняться строго один за другим, как написано.
При открытом отладчике (или, особенно, Firebug’е) это на практике соблюдается не всегда.
maolo
15.03.2017 09:06+1Кантор пишет, что defer, в отличие от async, влияет на порядок выполнения — Асинхронные скрипты: defer/async
браузер гарантирует, что относительный порядок скриптов с defer будет сохранён.
Tom910
15.03.2017 23:16На самом деле не так, в firefox и ie порядок исполнения может меняться — defer и firefox
mayorovp
16.03.2017 07:09+2Там описан серьезный баг, когда два скрипта начинают выполняться одновременно. Его давно починили.
funca
20.03.2017 09:51+2Кантор пишет отличные статьи. Но давайте посмотрим как на самом деле. Вот что написано в стандарте:
If the element has a src attribute, and the element has a defer attribute, and the element has been flagged as «parser-inserted», and the element does not have an async attribute
The element must be added to the end of the list of scripts that will execute when the document has finished parsing associated with the Document of the parser that created the element.
The task that the networking task source places on the task queue once the fetching algorithm has completed must set the element's «ready to be parser-executed» flag. The parser will handle executing the script.
Тут сказано, что defer скрипты выполняются после парсинга документа, в по мере их загрузки. Кто загрузится первым, тот и выполнится. Только в IE<=9 порядок выполнения был предсказуем, но это поведение не соответствует стандарту и в следующих версиях его починили.tenbits
21.03.2017 02:52+1Не однозначно, сказано же, что скрипт добавляется в конец списка скриптов, который (список) будет исполнен после окончание парсинга документа. Сеть же загружает скрипты и помечает их как готовые к выполнению (а не передает на выполнение). В заблуждение вводит последняя фраза, так как не явно указано, или парсер сразу выполнит скрипт, или выполнит только когда дойдёт до него очередь. Но из параграфа выше всё же следует, что парсер выполняет скрипты поочерёдно, иначе бы спецификация явно не указывала бы, что скрипт нужно добавить в список, тем более вконец — в этом не было бы смысла. Ну и ребята из html5rocks о том же говорят.
garfik
13.03.2017 22:16+2С defer надо быть весьма осторожным, так как до тех пор пока не загрузятся скрипты с defer, не отработает событие domcontentloaded, а некоторые фреймворки, типа Angular, на это завязаны. И в случае если скрипт отдается очень долго, то и приложение все это время не будет работать.
kirillaristov
14.03.2017 00:25Например, перед
подключаются jquery, jquery-ui, common.js.</body>
Их просто подключать один за одним, либо указывать всем (asynk || asynk & defer || defer)?tenbits
21.03.2017 17:07-2Смело оставляйте как есть, без defer/async. Браузеры не парсинг html останавливают, а обработку html. То-есть они вполне знают какие скрипты следуют за актуальным и грузят их параллельно. И даже напротив,
defer
в хроме замедляет начало их загрузки, по видимому тратиться время на создание отдельного queue
tenbits
22.03.2017 11:57-1Без defer скрипты грузятся параллельно. С defer создается отдельная очередь, хотя скрипты тоже грузятся параллельно. Преимуществ defer, если скрипты в конце документа попросту нет.
HKA
17.03.2017 15:14Например, в V8 (используется в Chromium), сделана попытка разобрать все скрипты, независимо от их атрибутов, на отдельном выделенном потоке для выполнения скрипта. Таким образом, «блокирующая парсер» природа JavaScript-файлов должна быть минимизирована по умолчанию.
Блокировку можно уменьшить за счет устранения препарсинга.
i360u
Еще интересен вариант сочетания http/2 и defer: загрузка скрипта и парсинг HTML — могут происходить одновременно, а запуск произойдет в момент, когда HTML будет готов, но раньше, чем это произошло бы при синхронном формировании запросов скриптов по ходу парсинга.