Асинхронный JavaScript против отложенного

https://bitsofco.de/async-vs-defer/
  • Перевод

В моей статье «Понимание критического пути рендеринга» (перевод статьи) я писала о том, какой эффект оказывают 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-файлов должна быть минимизирована по умолчанию.

Метки:
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 42
  • +1

    Еще интересен вариант сочетания http/2 и defer: загрузка скрипта и парсинг HTML — могут происходить одновременно, а запуск произойдет в момент, когда HTML будет готов, но раньше, чем это произошло бы при синхронном формировании запросов скриптов по ходу парсинга.

    • 0
      Когда парсер доходит до тега

      Является ли блокирующим следующее объявление?

      <script type="text/x-template" id="question">
          <div>
              content
          </div>
      </script>
      


      Где лучше размещать подобные блоки, в конце страницы или где угодно?
      • 0

        На сколько я понимаю — является. Если стоит задача использовать шаблоны, то я бы выбрал это

        • +3

          Тут нет ни загрузки сетевого ресурса, ни исполнения тяжелого кода, а потому вопрос является ли это объявление блокирующим — смысла не имеет.

          • 0

            Но парсер-то остановится, вопрос лишь в том, на долго ли (нет).

            • +11

              Нет. Парсер после полного чтения тэга сразу же пойдет дальше.


              Эдак можно и обычный тэг <div> блокирующим объявить — ну а как же, парсер же останавливается чтобы его разобрать?

        • 0
          Старый я стал, не понимаю, зачем всё это? desktop.min.js и mobile.min.js — ответ на все вопросы, во всяком случае множество разных проблем заменяется на одну — юзеры с медленным соединением, для которых 2 мегабайта при каждом обновлении — это трудно. А уж если за 2 мега вылезли или обновления ежедневно — значит что то делаем не так.
          • 0

            Нет, это не вы старый, это пост устарел :-) Сейчас как раз склеивание всех скриптов в один файл с его минификацией куда более распространено, чем раньше. requirejs, systemjs, webpack — все они обходят рассмотренную тут проблему, делая ее не такой существенной.

            • 0

              Как сказать. Выделение критических ресурсов для более быстрой отрисовки входит в обиход. К примеру, зачем мне ждать загрузки какой-нибудь карты, если я хочу просто ознакомиться с сайтом (например, сайт какой-нибудь компании) и могу до неё никогда и не дойти? Да и на всякие соц. виджеты может быть наплевать.

              • 0

                Да, но при использовании современных инструментов экосистемы javascript выделение критических ресурсов делается совсем другими методами, имеющими мало общего с атрибутами async и defer.

                • 0

                  Хм. Простой пример. Вот есть скрипт гугловый, который подключает карту. Вот есть мой некий common.js, в котором, скажем, описан какой-нибудь полифилл. Сайт встречает заголовком, текстом, формой, кучей текста, картинками и лишь в конце картой. Так вот как раз-таки для скрипта карты и пригодится defer. Гугл, кстати, так и рекомендует подключать у себя в примере.

                  • 0

                    … и получаем скрипт, который полностью работоспособен только если на тут же самую страницу подключен другой скрипт? Ну, и еще пара десятков сторонних скриптов?


                    От этого и стараются уйти, в том числе перечисленными мною инструментами.

                    • 0

                      Мы ускоряем отрисовку, а не ждём пока у нас загрузятся все ресурсы, которые, возможно, нам и не понадобятся. Чанки о том же, по-сути. Можно собрать всё в единый файл и пусть грузится или же разбить на части и грузить первыми только важные ресурсы, а остальные оставить на потом. Как минимум пользователь уже начнёт взаимодействовать с сайтом, а не будет ждать со словами «сайт тормозной». А, может, и вовсе уйдёт.

                      • 0

                        Суть одна — но механизмы-то разные! Вы понимаете, что выделение чанков в вебпаке совсем не похоже на простановку атрибутов async или defer скриптам?


                        Можно прочитать хоть сотню постов про атрибуты async и defer — но когда понадобится разбить пакет на чанки — придется лезть в документацию. И обратное тоже верно.

                        • 0

                          Не похоже. Мы разделили ресурсы, а затем сказали браузеру какой ресурс критичен, а какой нет.

          • 0
            Хотелось бы уточнить по поводу сохранения или не сохранения порядка выполнения нескольких скоиптов в случае с async и defer

            Например, если есть большой внешний скрипт и небольшой инлайновый сразу после него, то можно ли сделать внешний скрипт deferred?
            • 0
              В общем случае стоит считать, что порядок выполнения скриптов не гарантируется ни для defer ни для async.
            • 0
              Если внешний JavaScript-файл размещается непосредственно перед закрывающим тегом body, то использование async и defer становится менее уместным

              Менее уместно, но всё же уместно? Например, если у нас 5 внешних скриптов в конце body то без defer, они будут загружаться один за другим (так как парсер соответственно поочередно будет переходить от одного тэга script к другому). А вот с defer у каждого тэга можно загрузку распараллелить. Я правильно понимаю?

              • +1

                Смотрите какая штука.



                Вот таким образом он забрал файлы.
                А вот таким выполнил:


                • 0

                  Уточните пожалуйста, из вашего примера, все 3 скрипта с defer атрибутом или без?

                  • +2

                    Все 3 имеют defer.

                    • 0
                      Но ведь на втором скрине не видно, какой именно скрипт выполняется? Может они так и идут — 1,2,3? Или суть в том, что они отрабатывают за разное время? Тем самым 2 и 3 выполняются раньше.
                • +1

                  При этом в FF картина следующая:


                  • 0
                    defer влияет на порядок выполнения. Скрипты без атрибута будут выполняться строго один за другим, как написано.

                    При наличии атрибута defer порядок выполнения не определен. Кроме того, в defer скриптах нельзя выполнять document.write().
                    • +1
                      Скрипты без атрибута будут выполняться строго один за другим, как написано.


                      При открытом отладчике (или, особенно, Firebug’е) это на практике соблюдается не всегда.
                      • +1
                        Кантор пишет, что defer, в отличие от async, влияет на порядок выполнения — Асинхронные скрипты: defer/async
                        браузер гарантирует, что относительный порядок скриптов с defer будет сохранён.
                        • 0
                          На самом деле не так, в firefox и ie порядок исполнения может меняться — defer и firefox
                          • +2

                            Там описан серьезный баг, когда два скрипта начинают выполняться одновременно. Его давно починили.

                          • +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 порядок выполнения был предсказуем, но это поведение не соответствует стандарту и в следующих версиях его починили.
                            • +1

                              Не однозначно, сказано же, что скрипт добавляется в конец списка скриптов, который (список) будет исполнен после окончание парсинга документа. Сеть же загружает скрипты и помечает их как готовые к выполнению (а не передает на выполнение). В заблуждение вводит последняя фраза, так как не явно указано, или парсер сразу выполнит скрипт, или выполнит только когда дойдёт до него очередь. Но из параграфа выше всё же следует, что парсер выполняет скрипты поочерёдно, иначе бы спецификация явно не указывала бы, что скрипт нужно добавить в список, тем более вконец — в этом не было бы смысла. Ну и ребята из html5rocks о том же говорят.

                              • 0
                                Глядя на свежие Firefox 52 и Chrome 57, ваша трактовка похожа на правду.
                      • +2
                        С defer надо быть весьма осторожным, так как до тех пор пока не загрузятся скрипты с defer, не отработает событие domcontentloaded, а некоторые фреймворки, типа Angular, на это завязаны. И в случае если скрипт отдается очень долго, то и приложение все это время не будет работать.
                        • +2

                          если скрипт отдается очень долго, то ему ничего не поможет. даже если объявить его не defer, он все равно отодвинет domcontentloaded

                          • 0

                            Поможет загрузка этого скрипта из кода уже после domcontentloaded...

                        • 0
                          Например, перед
                          </body>
                          
                          подключаются jquery, jquery-ui, common.js.
                          Их просто подключать один за одним, либо указывать всем (asynk || asynk & defer || defer)?
                          • –2

                            Смело оставляйте как есть, без defer/async. Браузеры не парсинг html останавливают, а обработку html. То-есть они вполне знают какие скрипты следуют за актуальным и грузят их параллельно. И даже напротив, defer в хроме замедляет начало их загрузки, по видимому тратиться время на создание отдельного queue

                            • –1

                              Без defer скрипты грузятся параллельно. С defer создается отдельная очередь, хотя скрипты тоже грузятся параллельно. Преимуществ defer, если скрипты в конце документа попросту нет.



                              • 0
                                Например, в V8 (используется в Chromium), сделана попытка разобрать все скрипты, независимо от их атрибутов, на отдельном выделенном потоке для выполнения скрипта. Таким образом, «блокирующая парсер» природа JavaScript-файлов должна быть минимизирована по умолчанию.

                                Блокировку можно уменьшить за счет устранения препарсинга.
                                • –1

                                  В графике для async ошибка, нет там никакой блокировки парсинга. Читайте Илью Григорика https://developers.google.com/web/fundamentals/performance/critical-rendering-path/analyzing-crp

                                  • +1

                                    Конечно же, блокировка есть. Любой скрипт может взаимодействовать с DOM и ожидает, что одновременно с его работой DOM меняться не будет. Поэтому парсер не имеет возможности достраивать DOM в процессе исполнения скрипта.


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


                                    PS по приведенной вами ссылке этот вопрос воовсе не затрагивался

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

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

                                    Кто-то рассказывал про заглушку для функции $, которая выполнит все еще раз, когда jquery загрузится, но я что-то ее не нашел

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.