Загрузка больших ресурсов 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 по-прежнему предпочтительнее с точки зрения производительности загрузки.
Не отключайте случайно потоковую компиляцию
Движок Chromium V8 JavaScript предлагает ряд готовых оптимизаций, чтобы гарантировать, что ваш код 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
импортом.
По умолчанию, всякий раз, когда Webpack встречает динамический 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');
}
Конфигурация webpack по умолчанию для предыдущего фрагмента кода приводит к двум отдельным фрагментам:
- Кусок
main.js
, который Webpack классифицирует какinitial
кусок, включающийmain.js
и модуль./my-function.js
. -
async
фрагмент, включающий толькоform-validation.js
(содержащий хэш файла в имени ресурса, если настроен). Этот фрагмент загружается только тогда, когдаcondition
истинно .
Эта конфигурация позволяет отложить загрузку блока form-validation.js
до тех пор, пока он действительно не понадобится. Это может улучшить скорость реагирования на загрузку за счет сокращения времени оценки скрипта во время начальной загрузки страницы. Загрузка скрипта и оценка для блока form-validation.js
происходит при выполнении указанного условия, в этом случае динамически импортируемый модуль загружается. Одним из примеров может быть условие, когда полифил загружается только для определенного браузера, или — как в предыдущем примере — импортированный модуль необходим для взаимодействия с пользователем.
С другой стороны, изменение конфигурации SplitChunksPlugin
для указания chunks: initial
гарантирует, что код будет разделен только на начальные chunks. Это такие chunks, которые импортируются статически или перечислены в свойстве entry
webpack. Если посмотреть на предыдущий пример, то полученный chunk будет комбинацией form-validation.js
и main.js
в одном файле скрипта, что может привести к потенциальному ухудшению начальной производительности загрузки страницы.
Параметры SplitChunksPlugin
также можно настроить для разделения больших скриптов на несколько более мелких — например, используя параметр maxSize
, чтобы указать Webpack разбить фрагменты на отдельные файлы, если они превышают указанный параметр maxSize
. Разделение больших файлов скриптов на более мелкие файлы может улучшить отклик нагрузки , поскольку в некоторых случаях работа по оценке скриптов, требующая интенсивного использования ЦП, делится на более мелкие задачи, которые с меньшей вероятностью заблокируют основной поток на более длительные периоды времени.
Кроме того, создание больших файлов JavaScript также означает, что скрипты с большей вероятностью будут страдать от недействительности кэша. Например, если вы отправляете очень большой скрипт с кодом как фреймворка, так и приложения первой стороны, весь пакет может быть недействительным, если обновляется только фреймворк, но ничего больше в связанном ресурсе.
С другой стороны, меньшие файлы скриптов увеличивают вероятность того, что повторный посетитель извлечет ресурсы из кэша, что приводит к более быстрой загрузке страниц при повторных посещениях. Однако меньшие файлы получают меньше пользы от сжатия, чем большие, и могут увеличить время кругового обхода сети при загрузке страниц с неподготовленным кэшем браузера. Необходимо соблюдать осторожность, чтобы найти баланс между эффективностью кэширования, эффективностью сжатия и временем оценки скрипта.
демо-версия веб-пакета
Демо-версия Webpack SplitChunksPlugin
.
Проверьте свои знания
Какой тип оператора import
используется при выполнении разделения кода?
import()
.import
. Какой тип оператора import
должен находиться в верхней части модуля JavaScript и ни в каком другом месте?
import()
.import
. В чем разница между async
фрагментом и initial
фрагментом при использовании SplitChunksPlugin
в Webpack?
async
фрагменты загружаются с помощью динамического import()
, а initial
фрагменты загружаются с помощью статического import
.async
фрагменты загружаются с помощью статического import
, а initial
фрагменты загружаются с помощью динамического import()
. Далее: Ленивая загрузка изображений и элементов <iframe>
Хотя это, как правило, довольно дорогой тип ресурса, JavaScript — не единственный тип ресурса, загрузку которого можно отложить. Элементы изображений и <iframe>
сами по себе являются потенциально дорогими ресурсами. Подобно JavaScript, вы можете отложить загрузку изображений и элемента <iframe>
, используя их ленивую загрузку , что объясняется в следующем модуле этого курса.