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

Новая стратегия фрагментации Webpack в Next.js и Gatsby минимизирует дублирование кода для повышения производительности загрузки страницы.

Хуссейн Джирдех
Houssein Djirdeh

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

Введение

Как и многие веб-фреймворки, Next.js и Gatsby используют Webpack в качестве основного сборщика. В Webpack v3 появился CommonsChunkPlugin , позволяющий выводить модули, общие для разных точек входа, в виде одного (или нескольких) «общедоступных» фрагментов. Общий код можно загрузить отдельно и сохранить в кэше браузера на раннем этапе, что может повысить скорость загрузки.

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

Общая точка входа и конфигурация пакета

Несмотря на практичность, концепция объединения всего общего кода модулей в один блок имеет свои ограничения. Модули, не используемые совместно в каждой точке входа, могут загружаться по маршрутам, которые их не используют, что приводит к загрузке большего объёма кода, чем необходимо. Например, когда page1 загружает common блок, она загружает код для moduleC , хотя page1 не использует moduleC . По этой причине, наряду с несколькими другими, в Webpack v4 этот плагин был удалён в пользу нового: SplitChunksPlugin .

Улучшенное разделение на части

Настройки SplitChunksPlugin по умолчанию подходят большинству пользователей. В зависимости от ряда условий создаются несколько разделённых фрагментов, чтобы предотвратить дублирование кода по нескольким маршрутам.

Однако многие веб-фреймворки, использующие этот плагин, по-прежнему придерживаются подхода «единого общего» (single-common) к разделению фрагментов. Например, 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)[\\/]/,
      },

Оптимизировав конфигурацию Webpack для внедрения аналогичной стратегии гранулярной разбивки на фрагменты, они также заметили значительное сокращение объема 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 , чтобы понять, как они реализовали эту логику в своей конфигурации Webpack, которая поставляется по умолчанию в версии 2.20.7.

Заключение

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

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