JavaScript с разделением кода

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

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

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

Уменьшите анализ и выполнение JavaScript во время запуска за счет разделения кода.

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

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

Аудит Lighthouse, который сообщает о времени, затраченном на выполнение каждого файла JavaScript, запрашиваемого вашей страницей, полезен тем, что может помочь вам точно определить, какие сценарии могут быть кандидатами на разделение кода . Затем вы можете пойти дальше, используя инструмент покрытия в Chrome DevTools, чтобы точно определить, какие части JavaScript страницы не используются во время загрузки страницы.

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

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

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

document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
  // Get the form validation named export from the module through destructuring:
  const { validateForm } = await import('/validate-form.mjs');

  // Validate the form:
  validateForm();
}, { once: true });

В предыдущем фрагменте JavaScript модуль validate-form.mjs загружается, анализируется и выполняется только тогда, когда пользователь размывает любое из полей <input> формы. В этой ситуации ресурс JavaScript, отвечающий за управление логикой проверки формы, используется со страницей только тогда, когда она, скорее всего, будет фактически использована.

Упаковщики JavaScript, такие как webpack , Parcel , Rollup и esbuild , можно настроить на разделение пакетов JavaScript на более мелкие фрагменты всякий раз, когда они сталкиваются с динамическим вызовом import() в исходном коде. Большинство этих инструментов делают это автоматически, но esbuild, в частности, требует, чтобы вы согласились на эту оптимизацию.

Полезные замечания по разделению кода

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

Используйте сборщик, если можете

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

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

Сборщики также позволяют избежать проблемы доставки большого количества несвязанных модулей по сети. Архитектуры, использующие модули JavaScript, обычно имеют большие и сложные деревья модулей. Когда деревья модулей разделены, каждый модуль представляет собой отдельный HTTP-запрос, и интерактивность вашего веб-приложения может быть задержана, если вы не объединяете модули. Хотя можно использовать подсказку ресурса <link rel="modulepreload"> для загрузки больших деревьев модулей как можно раньше, пакеты JavaScript по-прежнему предпочтительнее с точки зрения производительности загрузки.

Не отключайте случайно потоковую компиляцию.

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

У вас есть несколько способов обеспечить потоковую компиляцию вашего веб-приложения в Chromium:

  • Преобразуйте свой производственный код, чтобы избежать использования модулей JavaScript. Бандлеры могут преобразовать исходный код JavaScript на основе цели компиляции, причем цель часто зависит от конкретной среды. V8 будет применять потоковую компиляцию к любому коду JavaScript, который не использует модули, и вы можете настроить свой упаковщик для преобразования кода вашего модуля JavaScript в синтаксис, который не использует модули JavaScript и их функции.
  • Если вы хотите отправлять модули JavaScript в производство, используйте расширение .mjs . Независимо от того, использует ли ваш производственный JavaScript модули или нет, не существует специального типа контента для JavaScript, который использует модули, по сравнению с JavaScript, который не использует модули. Что касается V8, вы фактически отказываетесь от потоковой компиляции при отправке модулей JavaScript в производство с использованием расширения .js . Если вы используете расширение .mjs для модулей JavaScript, V8 может гарантировать, что потоковая компиляция для кода JavaScript на основе модулей не будет нарушена.

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

Демонстрация динамического импорта

веб-пакет

webpack поставляется с плагином SplitChunksPlugin , который позволяет вам настроить способ разделения файлов JavaScript сборщиком. webpack распознает как динамический import() , так и статический оператор import . Поведение SplitChunksPlugin можно изменить, указав опцию chunks в его конфигурации:

  • chunks: async — значение по умолчанию и относится к динамическим вызовам import() .
  • chunks: initial относится к статическим вызовам import .
  • chunks: all охватывает как динамический import() так и статический импорт, что позволяет разделять фрагменты между async и initial импортом.

По умолчанию всякий раз, когда веб-пакет встречает динамический оператор import() . он создает отдельный чанк для этого модуля:

/* main.js */

// An application-specific chunk required during the initial page load:
import myFunction from './my-function.js';

myFunction('Hello world!');

// If a specific condition is met, a separate chunk is downloaded on demand,
// rather than being bundled with the initial chunk:
if (condition) {
  // Assumes top-level await is available. More info:
  // https://v8.dev/features/top-level-await
  await import('/form-validation.js');
}

Конфигурация веб-пакета по умолчанию для предыдущего фрагмента кода приводит к созданию двух отдельных фрагментов:

  • Чанк main.js , который веб-пакет классифицирует как initial чанк, включает в себя модуль main.js и ./my-function.js .
  • async фрагмент, который включает только form-validation.js (содержащий хэш файла в имени ресурса, если он настроен). Этот фрагмент загружается только в том случае, если condition истинно .

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

С другой стороны, изменение конфигурации SplitChunksPlugin для указания chunks: initial гарантирует, что код разбивается только на начальные фрагменты. Это фрагменты, например статически импортированные или перечисленные в свойстве entry веб-пакета. Если посмотреть на предыдущий пример, то результирующий фрагмент будет представлять собой комбинацию form-validation.js и main.js в одном файле сценария, что потенциально приведет к снижению производительности начальной загрузки страницы.

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

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

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

демо веб-пакета

Демонстрация веб-пакета SplitChunksPlugin .

Проверьте свои знания

Какой тип оператора import используется при разделении кода?

Динамический import() .
Правильный!
Статический import .
Попробуйте еще раз.

Какой тип оператора import должен находиться в верхней части модуля JavaScript и ни в каком другом месте?

Динамический import() .
Попробуйте еще раз.
Статический import .
Правильный!

В чем разница между async чанком и initial чанком при использовании SplitChunksPlugin в веб-пакете?

async фрагменты загружаются с помощью динамического import() , а initial фрагменты загружаются с помощью статического import .
Правильный!
async фрагменты загружаются с помощью статического import , а initial фрагменты загружаются с помощью динамического import() .
Попробуйте еще раз.

Далее: отложенная загрузка изображений и элементов <iframe>

Хотя JavaScript, как правило, является довольно дорогим типом ресурсов, он не единственный тип ресурсов, загрузку которого можно отложить. Элементы Image и <iframe> сами по себе являются потенциально дорогостоящими ресурсами. Как и в случае с JavaScript, вы можете отложить загрузку изображений и элемента <iframe> , отложив их загрузку , что объясняется в следующем модуле этого курса.