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

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

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

Введение

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

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

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

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

Улучшенное разбиение на фрагменты

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

Однако многие веб-фреймворки, использующие этот плагин, по-прежнему используют подход "single-commons" для разделения фрагментов. Например, 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 КБ) разбивается на отдельные фрагменты.
  • Для зависимостей фреймворка (react, react-dom и т. д.) создается отдельный фрагмент frameworks.
  • Создается необходимое количество общих фрагментов (до 25).
  • Минимальный размер фрагмента, который можно сгенерировать, изменен на 20 КБ.

Такая стратегия разбиения на фрагменты дает следующие преимущества:

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

Полную конфигурацию, которую использует Next.js, можно посмотреть в файле webpack-config.ts.

Больше HTTP-запросов

SplitChunksPlugin определил основу для детального разбиения на фрагменты, и применение этого подхода к фреймворку, такому как Next.js, не было совершенно новой концепцией. Однако многие фреймворки по-прежнему использовали единую эвристику и стратегию пакета "commons" по нескольким причинам. В частности, они опасаются, что большое количество HTTP-запросов может негативно повлиять на производительность сайта.

Браузеры могут открывать только ограниченное количество TCP-подключений к одному источнику (6 для Chrome), поэтому минимизация количества фрагментов, выводимых сборщиком, может гарантировать, что общее количество запросов останется ниже этого порога. Однако это справедливо только для HTTP/1.1. Мультиплексирование в HTTP/2 позволяет параллельно передавать несколько запросов, используя одно подключение к одному источнику. Другими словами, обычно нам не нужно беспокоиться об ограничении количества фрагментов, создаваемых сборщиком.

Протокол HTTP/2 поддерживается всеми основными браузерами. Команды Chrome и Next.js хотели проверить, повлияет ли увеличение количества запросов за счет разделения одного общего пакета Next.js на несколько общих фрагментов на производительность загрузки. Сначала они измерили эффективность одного сайта, изменяя максимальное количество параллельных запросов с помощью свойства maxInitialRequests.

Скорость загрузки страниц при увеличенном количестве запросов

В среднем за три запуска нескольких тестов на одной веб-странице время load, начала отрисовки и первой отрисовки контента оставалось примерно одинаковым при изменении максимального количества начальных запросов (от 5 до 15). Интересно, что мы заметили небольшое снижение производительности только после агрессивного разделения на сотни запросов.

Скорость загрузки страницы с сотнями запросов

Это показало, что соблюдение надежного порога (20–25 запросов) обеспечивает правильный баланс между производительностью загрузки и эффективностью кэширования. После некоторых базовых тестов 25 было выбрано в качестве количества maxInitialRequest.

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

Уменьшение полезной нагрузки JavaScript за счет увеличения количества фрагментов

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

webpack использует 30 КБ в качестве минимального размера по умолчанию для генерируемого фрагмента. Однако сочетание значения maxInitialRequests, равного 25, с минимальным размером 20 КБ позволило улучшить кеширование.

Уменьшение размера за счет разбиения на фрагменты

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

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.

Gatsby

Gatsby использовал тот же подход, основанный на эвристике использования, для определения общих модулей:

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 или даже веб-пакета. Всем следует подумать об улучшении стратегии фрагментации своего приложения, если оно использует подход с большим "общим" пакетом, независимо от используемой платформы или упаковщика модулей.

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