Оценка сценария и длинные задачи

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

Когда дело доходит до оптимизации взаимодействия до следующей отрисовки (INP) , большинство советов, которые вы встретите, — это оптимизация самих взаимодействий. Например, в руководстве по оптимизации длинных задач обсуждаются такие методы, как yielding с setTimeout и другие. Эти методы полезны, так как они дают основному потоку некоторую передышку, избегая длинных задач, что может дать больше возможностей для более быстрого выполнения взаимодействий и другой активности, чем если бы им пришлось ждать одну длинную задачу.

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

Что такое оценка сценария?

Если вы профилировали приложение, которое поставляет много JavaScript, вы могли видеть длительные задачи, где виновником является Evaluate Script .

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

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

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

Связь между сценариями и задачами, которые их оценивают

То, как запускаются задачи, отвечающие за оценку скрипта, зависит от того, загружен ли загружаемый скрипт с помощью типичного элемента <script> или скрипт является модулем, загруженным с помощью type=module . Поскольку браузеры склонны обрабатывать вещи по-разному, то, как основные браузерные движки обрабатывают оценку скрипта, будет затронуто в тех случаях, когда поведение оценки скрипта у них различается.

Скрипты, загруженные с помощью элемента <script>

Количество задач, отправляемых для оценки скриптов, обычно напрямую связано с количеством элементов <script> на странице. Каждый элемент <script> запускает задачу по оценке запрошенного скрипта, чтобы его можно было проанализировать, скомпилировать и выполнить. Это касается браузеров на основе Chromium, Safari и Firefox.

Почему это важно? Допустим, вы используете сборщик для управления своими производственными скриптами и настроили его на объединение всего, что нужно вашей странице для запуска, в один скрипт. Если это касается вашего веб-сайта, вы можете ожидать, что будет отправлена ​​одна задача для оценки этого скрипта. Это плохо? Не обязательно, если только этот скрипт не огромный .

Вы можете разбить работу по оценке скрипта, избежав загрузки больших фрагментов JavaScript, и загружать больше отдельных, более мелких скриптов, используя дополнительные элементы <script> .

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

Несколько задач, включающих оценку скрипта, как визуализируется в профилировщике производительности Chrome DevTools. Поскольку загружаются несколько небольших скриптов вместо нескольких больших скриптов, задачи с меньшей вероятностью станут длинными задачами, что позволяет основному потоку быстрее реагировать на пользовательский ввод.
Несколько задач, созданных для оценки скриптов, как результат нескольких элементов <script> , присутствующих в HTML страницы. Это предпочтительнее, чем отправлять пользователям один большой пакет скриптов, что с большей вероятностью заблокирует основной поток.

Вы можете представить себе разбиение задач для оценки скрипта как нечто похожее на yielding во время обратных вызовов событий, которые выполняются во время взаимодействия . Однако при оценке скрипта механизм yielding разбивает загружаемый вами JavaScript на несколько меньших скриптов, а не на меньшее количество больших скриптов, которые с большей вероятностью заблокируют основной поток.

Скрипты, загруженные с элементом <script> и атрибутом type=module

Теперь можно загружать модули ES в браузере с помощью атрибута type=module в элементе <script> . Такой подход к загрузке скриптов имеет некоторые преимущества для разработчиков, например, отсутствие необходимости преобразовывать код для использования в производстве, особенно при использовании в сочетании с import maps . Однако загрузка скриптов таким образом планирует задачи, которые различаются в зависимости от браузера.

Браузеры на базе Chromium

В таких браузерах, как Chrome (или производных от него) загрузка модулей ES с использованием атрибута type=module создает другие виды задач, чем те, которые вы обычно видите, не используя type=module . Например, будет запущена задача для каждого скрипта модуля, которая включает в себя активность, помеченную как Compile module .

Компиляция модуля работает в нескольких задачах, как показано в Chrome DevTools.
Поведение загрузки модуля в браузерах на базе Chromium. Каждый скрипт модуля будет порождать вызов Compile module для компиляции их содержимого перед оценкой.

После компиляции модулей любой код, который впоследствии будет запущен в них, запустит действие, помеченное как Evaluate module .

Оценка модуля «точно в срок», визуализируемая на панели производительности Chrome DevTools.
При запуске кода в модуле этот модуль будет оценен «точно в срок».

Эффект здесь — по крайней мере, в Chrome и связанных браузерах — заключается в том, что этапы компиляции разбиваются при использовании модулей ES. Это явный выигрыш с точки зрения управления длительными задачами; однако, в результате работы по оценке модулей вы все равно несете некоторые неизбежные затраты. Хотя вам следует стремиться отправлять как можно меньше JavaScript, использование модулей ES — независимо от браузера — обеспечивает следующие преимущества:

  • Весь код модуля автоматически запускается в строгом режиме , что позволяет движкам JavaScript выполнять потенциальные оптимизации, которые невозможно было бы выполнить в нестрогом контексте.
  • Скрипты, загруженные с использованием type=module , обрабатываются так, как если бы они были отложены по умолчанию. Можно использовать атрибут async для скриптов, загруженных с type=module чтобы изменить это поведение.

Safari и Firefox

Когда модули загружаются в Safari и Firefox, каждый из них оценивается в отдельной задаче. Это означает, что теоретически вы можете загрузить один модуль верхнего уровня, состоящий только из статических операторов import в другие модули, и каждый загруженный модуль повлечет за собой отдельный сетевой запрос и задачу для его оценки.

Скрипты, загруженные с помощью динамического import()

Dynamic import() — еще один метод загрузки скриптов. В отличие от статических операторов import , которые должны находиться в верхней части модуля ES, вызов dynamic import() может появляться в любом месте скрипта для загрузки фрагмента JavaScript по требованию. Этот метод называется разделением кода .

Динамический import() имеет два преимущества с точки зрения улучшения INP:

  1. Модули, которые откладываются для загрузки позже, уменьшают конкуренцию основного потока во время запуска, уменьшая объем JavaScript, загружаемого в это время. Это освобождает основной поток, чтобы он мог лучше реагировать на взаимодействие с пользователем.
  2. Когда выполняются вызовы динамического import() , каждый вызов фактически разделит компиляцию и оценку каждого модуля на его собственную задачу. Конечно, динамический import() , загружающий очень большой модуль, запустит довольно большую задачу оценки скрипта, и это может помешать способности основного потока реагировать на ввод пользователя, если взаимодействие происходит одновременно с вызовом динамического import() . Поэтому по-прежнему очень важно загружать как можно меньше JavaScript.

Динамические вызовы import() ведут себя одинаково во всех основных браузерных движках: количество задач оценки скрипта, которые в результате будут выполнены, будет соответствовать количеству динамически импортируемых модулей.

Скрипты, загруженные в веб-воркер

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

Помимо сокращения работы основного потока, сами веб-воркеры могут загружать внешние скрипты для использования в контексте воркера либо через importScripts , либо через статические операторы import в браузерах, которые поддерживают module workers . Результатом является то, что любой скрипт, запрошенный веб-воркером, оценивается вне основного потока.

Компромиссы и соображения

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

Эффективность сжатия

Сжатие является фактором, когда дело доходит до разбиения скриптов. Когда скрипты меньше, сжатие становится несколько менее эффективным. Большие скрипты получат гораздо больше пользы от сжатия. Хотя увеличение эффективности сжатия помогает поддерживать время загрузки скриптов как можно ниже, это своего рода балансировка, чтобы гарантировать, что вы разбиваете скрипты на достаточно меньшие части, чтобы обеспечить лучшую интерактивность во время запуска.

Упаковщики — идеальные инструменты для управления выходным размером скриптов, от которых зависит ваш сайт:

  • Что касается webpack, его плагин SplitChunksPlugin может помочь. Ознакомьтесь с документацией SplitChunksPlugin для опций, которые вы можете задать для управления размерами ресурсов.
  • Для других сборщиков, таких как Rollup и esbuild , вы можете управлять размерами файлов скриптов, используя динамические вызовы import() в вашем коде. Эти сборщики, а также webpack, автоматически разбивают динамически импортированный актив на собственный файл, тем самым избегая больших начальных размеров пакета.

Аннулирование кэша

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

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

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

Если вы отправляете модули ES в производство и загружаете их с атрибутом type=module , вам нужно знать, как вложенность модулей может повлиять на время запуска. Вложенность модулей означает, что модуль ES статически импортирует другой модуль ES, который статически импортирует другой модуль ES:

// a.js
import {b} from './b.js';

// b.js
import {c} from './c.js';

Если ваши модули ES не связаны вместе, предыдущий код приводит к цепочке сетевых запросов: когда a.js запрашивается из элемента <script> , другой сетевой запрос отправляется для b.js , который затем включает другой запрос для c.js . Один из способов избежать этого — использовать сборщик, но убедитесь, что вы настраиваете сборщик для разбиения скриптов, чтобы распределить работу по оценке скриптов.

Если вы не хотите использовать сборщик, то другой способ обойти вложенные вызовы модулей — использовать подсказку ресурса modulepreload , которая заранее загрузит модули ES, чтобы избежать цепочек сетевых запросов.

Заключение

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

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

  • При загрузке скриптов с использованием элемента <script> без атрибута type=module избегайте загрузки скриптов, которые очень большие, так как они запускают ресурсоемкие задачи оценки скриптов, которые блокируют основной поток. Распределите свои скрипты по большему количеству элементов <script> , чтобы разбить эту работу.
  • Использование атрибута type=module для загрузки модулей ES напрямую в браузере запустит отдельные задачи оценки для каждого отдельного скрипта модуля.
  • Уменьшите размер ваших начальных пакетов, используя динамические вызовы import() . Это также работает в сборщиках, поскольку сборщики будут рассматривать каждый динамически импортированный модуль как «точку разделения», в результате чего для каждого динамически импортированного модуля будет сгенерирован отдельный скрипт.
  • Обязательно взвесьте компромиссы, такие как эффективность сжатия и аннулирование кэша. Большие скрипты будут сжиматься лучше, но с большей вероятностью потребуют более дорогостоящей работы по оценке скрипта в меньшем количестве задач и приведут к аннулированию кэша браузера, что приведет к общему снижению эффективности кэширования.
  • При использовании модулей ES изначально, без объединения, используйте подсказку ресурса modulepreload , чтобы оптимизировать их загрузку во время запуска.
  • Как всегда, старайтесь использовать как можно меньше JavaScript.

Это, конечно, балансирующий акт — но, разбивая скрипты и сокращая начальные полезные нагрузки с помощью динамического import() , вы можете добиться лучшей производительности запуска и лучше приспособиться к пользовательским взаимодействиям в этот критический период запуска. Это должно помочь вам получить более высокие баллы по метрике INP, тем самым обеспечивая лучший пользовательский опыт.