Улучшена производительность загрузки страниц Next.js и Gatsby за счет детального разбиения на блоки.

Новая стратегия разделения веб-пакетов в 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.

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

Сокращение полезной нагрузки JavaScript за счет увеличения фрагментации

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

Эта новая стратегия детального разбиения на фрагменты была впервые реализована в Next.js под флагом, где она была протестирована на ряде первых пользователей. Многие заметили значительное сокращение общего объема JavaScript, используемого для всего сайта:

Веб-сайт Общее изменение JS % Разница
https://www.barnebys.com/ -238 КБ -23%
https://sumup.com/ -220 КБ -30%
https://www.hashicorp.com/ -11 МБ -71%
Уменьшение размера JavaScript — по всем маршрутам (сжатым)

Окончательная версия по умолчанию поставляется в версии 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%
Уменьшение размера JavaScript — по всем маршрутам (сжатым)

Взгляните на PR , чтобы понять, как они реализовали эту логику в конфигурации своего веб-пакета, который по умолчанию поставляется в версии 2.20.7.

Заключение

Концепция доставки фрагментированных фрагментов не является специфичной для Next.js, Gatsby или даже веб-пакета. Каждому следует рассмотреть возможность улучшения стратегии разбивки своего приложения на фрагменты, если она следует подходу «больших общих» пакетов, независимо от используемой платформы или сборщика модулей.

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