Новая стратегия разделения веб-пакетов в Next.js и Gatsby сводит к минимуму дублирующийся код и повышает производительность загрузки страниц.
Chrome сотрудничает с инструментами и платформами в экосистеме JavaScript с открытым исходным кодом. Недавно был добавлен ряд новых оптимизаций для улучшения производительности загрузки Next.js и Gatsby . В этой статье рассматривается улучшенная стратегия детального разбиения на фрагменты, которая теперь поставляется по умолчанию в обеих платформах.
Введение
Как и многие веб-фреймворки, Next.js и Gatsby используют webpack в качестве основного сборщика. В webpack v3 введен CommonsChunkPlugin
, позволяющий выводить модули, совместно используемые различными точками входа, в один (или несколько) «общий» фрагмент (или фрагменты). Общий код можно загрузить отдельно и заранее сохранить в кеше браузера, что может привести к повышению производительности загрузки.
Этот шаблон стал популярным во многих средах одностраничных приложений, в которых точка входа и конфигурация пакета выглядели следующим образом:
Несмотря на практичность, концепция объединения всего кода общего модуля в один фрагмент имеет свои ограничения. Модули, не используемые в каждой точке входа, могут быть загружены для маршрутов, которые их не используют, в результате чего загружается больше кода, чем необходимо. Например, когда page1
загружает common
чанк, она загружает код для moduleC
даже если page1
не использует moduleC
. По этой причине, наряду с некоторыми другими, в webpack v4 удален плагин в пользу нового: SplitChunksPlugin
.
Улучшенное разбиение на части
Настройки по умолчанию для SplitChunksPlugin
подходят большинству пользователей. Несколько разделенных фрагментов создаются в зависимости от ряда условий , чтобы предотвратить получение дублированного кода по нескольким маршрутам.
Однако многие веб-фреймворки, использующие этот плагин, по-прежнему следуют подходу «единого общего» для разделения блоков. Например, Next.js будет генерировать пакет commons
, содержащий любой модуль, который используется более чем на 50% страниц, и все зависимости фреймворка ( react
, react-dom
и т. д.).
const splitChunksConfigs = {
…
prod: {
chunks: 'all',
cacheGroups: {
default: false,
vendors: false,
commons: {
name: 'commons',
chunks: 'all',
minChunks: totalPages > 2 ? totalPages * 0.5 : 2,
},
react: {
name: 'commons',
chunks: 'all',
test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-subscription)[\\/]/,
},
},
},
Хотя включение кода, зависящего от платформы, в общий фрагмент означает, что его можно загрузить и кэшировать для любой точки входа, эвристика на основе использования, заключающаяся в включении общих модулей, используемых более чем в половине страниц, не очень эффективна. Изменение этого соотношения приведет только к одному из двух результатов:
- Если вы уменьшите это соотношение, будет загружено больше ненужного кода.
- Если вы увеличите это соотношение, больше кода будет дублироваться на нескольких маршрутах.
Чтобы решить эту проблему, Next.js принял другую конфигурацию SplitChunksPlugin
, которая сокращает количество ненужного кода для любого маршрута.
- Любой достаточно большой сторонний модуль (более 160 КБ) разбивается на отдельный фрагмент.
- Для зависимостей фреймворка создается отдельный блок
frameworks
(react
,react-dom
и т. д.). - Создается столько общих фрагментов, сколько необходимо (до 25).
- Минимальный размер генерируемого фрагмента изменен на 20 КБ.
Эта детальная стратегия фрагментации обеспечивает следующие преимущества:
- Время загрузки страницы улучшено . Создание нескольких общих фрагментов вместо одного минимизирует количество ненужного (или дублирующего) кода для любой точки входа.
- Улучшено кеширование во время навигации . Разделение больших библиотек и зависимостей платформы на отдельные фрагменты снижает вероятность аннулирования кэша, поскольку они вряд ли изменятся до тех пор, пока не будет выполнено обновление.
Вы можете увидеть всю конфигурацию, принятую Next.js, в файле webpack-config.ts
.
Больше HTTP-запросов
SplitChunksPlugin
определил основу для детального разбиения на фрагменты, и применение этого подхода к такой платформе, как Next.js, не было совершенно новой концепцией. Однако многие платформы по-прежнему продолжали использовать единую эвристическую и «общую» стратегию пакетов по нескольким причинам. Это включает в себя опасения, что большое количество HTTP-запросов может отрицательно повлиять на производительность сайта.
Браузеры могут открывать только ограниченное количество TCP-соединений с одним источником (6 для Chrome), поэтому минимизация количества фрагментов, выводимых сборщиком, может гарантировать, что общее количество запросов останется ниже этого порога. Однако это справедливо только для HTTP/1.1. Мультиплексирование в HTTP/2 позволяет передавать несколько запросов параллельно, используя одно соединение через один источник. Другими словами, нам обычно не нужно беспокоиться об ограничении количества фрагментов, создаваемых нашим сборщиком.
Все основные браузеры поддерживают HTTP/2. Команды Chrome и Next.js хотели посмотреть, повлияет ли каким-либо образом увеличение количества запросов за счет разделения единого пакета Next.js на несколько общих фрагментов на производительность загрузки. Они начали с измерения производительности одного сайта, одновременно изменяя максимальное количество параллельных запросов с помощью свойства maxInitialRequests
.
В среднем за три запуска нескольких испытаний на одной веб-странице время load
, начала рендеринга и первой отрисовки контента оставалось примерно одинаковым при изменении максимального количества начальных запросов (от 5 до 15). Интересно, что мы заметили небольшое снижение производительности только после агрессивного разделения на сотни запросов.
Это показало, что соблюдение надежного порога (20–25 запросов) обеспечивает правильный баланс между производительностью загрузки и эффективностью кэширования. После некоторого базового тестирования в качестве счетчика maxInitialRequest
было выбрано 25.
Изменение максимального количества запросов, которые выполняются параллельно, привело к созданию более чем одного общего пакета, а их правильное разделение для каждой точки входа значительно сократило количество ненужного кода для одной и той же страницы.
Этот эксперимент заключался только в изменении количества запросов, чтобы увидеть, будет ли какое-либо негативное влияние на производительность загрузки страницы. Результаты показывают, что установка maxInitialRequests
на 25
на тестовой странице была оптимальной, поскольку она уменьшала размер полезной нагрузки JavaScript без замедления страницы. Общий объем JavaScript, необходимый для увлажнения страницы, остался примерно таким же, что объясняет, почему производительность загрузки страницы не обязательно улучшилась при уменьшении объема кода.
webpack использует 30 КБ в качестве минимального размера по умолчанию для создаваемого фрагмента. Однако сочетание значения maxInitialRequests
, равного 25, с минимальным размером 20 КБ вместо этого привело к улучшению кэширования.
Уменьшение размера за счет гранулированных кусков
Многие фреймворки, включая Next.js, полагаются на маршрутизацию на стороне клиента (управляемую JavaScript) для внедрения новых тегов сценария при каждом переходе маршрута. Но как они заранее определяют эти динамические фрагменты во время сборки?
Next.js использует файл манифеста сборки на стороне сервера, чтобы определить, какие выходные фрагменты используются различными точками входа. Чтобы предоставить эту информацию клиенту, был создан сокращенный файл манифеста сборки на стороне клиента, в котором отображаются все зависимости для каждой точки входа.
// Returns a promise for the dependencies for a particular route
getDependencies (route) {
return this.promisedBuildManifest.then(
man => (man[route] && man[route].map(url => `/_next/${url}`)) || []
)
}
Эта новая стратегия детального разбиения на фрагменты была впервые реализована в Next.js под флагом, где она была протестирована на ряде первых пользователей. Многие заметили значительное сокращение общего объема JavaScript, используемого для всего сайта:
Веб-сайт | Общее изменение JS | % Разница |
---|---|---|
https://www.barnebys.com/ | -238 КБ | -23% |
https://sumup.com/ | -220 КБ | -30% |
https://www.hashicorp.com/ | -11 МБ | -71% |
Окончательная версия по умолчанию поставляется в версии 9.2 .
Гэтсби
Гэтсби использовал тот же подход, используя эвристику на основе использования для определения общих модулей:
config.optimization = {
…
splitChunks: {
name: false,
chunks: `all`,
cacheGroups: {
default: false,
vendors: false,
commons: {
name: `commons`,
chunks: `all`,
// if a chunk is used more than half the components count,
// we can assume it's pretty global
minChunks: componentsCount > 2 ? componentsCount * 0.5 : 2,
},
react: {
name: `commons`,
chunks: `all`,
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
},
Оптимизировав конфигурацию веб-пакета для применения аналогичной стратегии детального разбиения на фрагменты, они также заметили значительное сокращение количества JavaScript на многих крупных сайтах:
Веб-сайт | Общее изменение JS | % Разница |
---|---|---|
https://www.gatsbyjs.org/ | -680 КБ | -22% |
https://www. Thirdandgrove.com/ | -390 КБ | -25% |
https://ghost.org/ | -1,1 МБ | -35% |
https://reactjs.org/ | -80 Кб | -8% |
Взгляните на PR , чтобы понять, как они реализовали эту логику в конфигурации своего веб-пакета, который по умолчанию поставляется в версии 2.20.7.
Заключение
Концепция доставки фрагментированных фрагментов не является специфичной для Next.js, Gatsby или даже веб-пакета. Каждому следует рассмотреть возможность улучшения стратегии разбивки своего приложения на фрагменты, если она следует подходу «больших общих» пакетов, независимо от используемой платформы или сборщика модулей.
- Если вы хотите, чтобы те же оптимизации разбиения на фрагменты были применены к обычному приложению React, взгляните на этот пример приложения React . Он использует упрощенную версию стратегии детального разбиения на фрагменты и может помочь вам начать применять ту же логику на своем сайте.
- Для Rollup по умолчанию фрагменты создаются гранулярно. Если вы хотите настроить поведение вручную, посмотрите
manualChunks
.