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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Браузеры на основе Chromium

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

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

После компиляции модулей любой код, который впоследствии будет выполняться в них, запустит процесс, помеченный как «Выполнить модуль» .

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

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

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

Safari и Firefox

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

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

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

Функция динамического import() имеет два преимущества в плане повышения эффективности импорта:

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

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

Скрипты загружаются в веб-воркер.

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

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

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

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

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

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

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

  • Что касается 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 , которая предварительно загрузит модули Elasticsearch, чтобы избежать цепочек сетевых запросов.

Заключение

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

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

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

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