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


  1. i360u
    13.03.2017 14:02
    +1

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


  1. mtt
    13.03.2017 14:46

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

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

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


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


    1. Binjo
      13.03.2017 14:52

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


    1. mayorovp
      13.03.2017 14:55
      +3

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


      1. Binjo
        13.03.2017 14:58

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


        1. mayorovp
          13.03.2017 15:00
          +11

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


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


  1. maniacscientist
    13.03.2017 15:41

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


    1. mayorovp
      13.03.2017 15:47

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


      1. Binjo
        13.03.2017 15:56

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


        1. mayorovp
          13.03.2017 16:00

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


          1. Binjo
            13.03.2017 16:07

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


            1. mayorovp
              13.03.2017 16:10

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


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


              1. Binjo
                13.03.2017 16:16

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


                1. mayorovp
                  13.03.2017 16:19

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


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


                  1. Binjo
                    13.03.2017 16:22

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


  1. KhodeN
    13.03.2017 17:06

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

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


    1. funca
      13.03.2017 22:59

      В общем случае стоит считать, что порядок выполнения скриптов не гарантируется ни для defer ни для async.


  1. tenbits
    13.03.2017 17:16

    Если внешний JavaScript-файл размещается непосредственно перед закрывающим тегом body, то использование async и defer становится менее уместным

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


    1. Binjo
      13.03.2017 17:47
      +1

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



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



      1. tenbits
        13.03.2017 18:01

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


        1. Binjo
          13.03.2017 18:06
          +2

          Все 3 имеют defer.


          1. kirillaristov
            14.03.2017 00:14

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


    1. Binjo
      13.03.2017 17:57
      +1

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



    1. funca
      13.03.2017 23:07

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

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


      1. torbasow
        14.03.2017 13:41
        +1

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


        При открытом отладчике (или, особенно, Firebug’е) это на практике соблюдается не всегда.


      1. maolo
        15.03.2017 09:06
        +1

        Кантор пишет, что defer, в отличие от async, влияет на порядок выполнения — Асинхронные скрипты: defer/async

        браузер гарантирует, что относительный порядок скриптов с defer будет сохранён.


        1. Tom910
          15.03.2017 23:16

          На самом деле не так, в firefox и ie порядок исполнения может меняться — defer и firefox


          1. mayorovp
            16.03.2017 07:09
            +2

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


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


          1. tenbits
            21.03.2017 02:52
            +1

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


            1. funca
              23.03.2017 07:43

              Глядя на свежие Firefox 52 и Chrome 57, ваша трактовка похожа на правду.


  1. garfik
    13.03.2017 22:16
    +2

    С defer надо быть весьма осторожным, так как до тех пор пока не загрузятся скрипты с defer, не отработает событие domcontentloaded, а некоторые фреймворки, типа Angular, на это завязаны. И в случае если скрипт отдается очень долго, то и приложение все это время не будет работать.


    1. Dreyk
      13.03.2017 23:47
      +2

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


      1. mayorovp
        14.03.2017 06:20

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


  1. kirillaristov
    14.03.2017 00:25

    Например, перед

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


    1. tenbits
      21.03.2017 17:07
      -2

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


    1. tenbits
      22.03.2017 11:57
      -1

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





  1. HKA
    17.03.2017 15:14

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

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