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

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

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

Однако как насчет длительных задач, связанных с загрузкой самих скриптов? Эти задачи могут мешать взаимодействию пользователя и влиять на 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-коде страницы. Это предпочтительнее, чем отправлять пользователям один большой пакет сценариев, который с большей вероятностью заблокирует основной поток.

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

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

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

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

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

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

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

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

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

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

Сафари и Фаерфокс

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

Загрузка скриптов с помощью динамического import()

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

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

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

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

Загрузка скриптов в веб-воркере

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

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

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

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

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

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

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

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

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

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

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

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

Если вы отправляете 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, тем самым обеспечивая лучший пользовательский опыт.

Героическое изображение из Unsplash , автор Маркус Списке .