Глубокое понимание упаковки и фрагментации Webpack (Часть 1)

JavaScript Webpack

предисловие

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

Однако, если подумать, этого вполне можно избежать: изменение одной строчки кода требует переупаковки всего скрипта во время разработки? Пользователям нужно только загрузить все скрипты всего сайта, чтобы примерно просмотреть страницу? Таким образом, тенденция должна заключаться в том, чтобы разделять и предоставлять код пользователям по требованию и стратегически. Популярные в последнее время микрофронтенды тоже в некотором смысле следуют этому принципу (но не совсем по этой причине).

К счастью, инструменты, которые у нас есть в настоящее время, полностью позволяют нам выполнить вышеуказанные требования. Например, Webpack позволяет нам блокировать скрипты при сборке, а с помощью кеширования браузера мы можем целенаправленно загружать активы.

В поисках лучших практик меня больше всего озадачило не то, сможем ли мы это сделать, а то, как мы должны это сделать: какие функции мы должны использовать, чтобы разделить сценарий? Какую стратегию кэширования мы должны использовать? Является ли использование ленивой загрузки и фрагментации одним и тем же? Насколько это может улучшить производительность после разделения? Самое главное, с таким количеством вариантов, инструментов и неопределенностей, с чего нам начать? Эта статья предназначена для того, чтобы разобраться и ответить на поставленные выше вопросы. Содержание статьи условно разделено на два аспекта: с одной стороны, стратегия разделения модулей формулируется с точки зрения идей, а с другой стороны, решение реализуется технически.

Основное содержание этой статьи переведено сThe 100% correct way to split your chunks with Webpack. Эта статья шаг за шагом помогает разработчикам разделить и оптимизировать код шаг за шагом, поэтому она существует как ключ к этой статье. Заодно на его основе буду вертикально расширять Webpack и другие точки знаний, и реализовывать задуманное.

Начать текст ниже


согласно сГлоссарий веб-пакетов, есть разделение на два типа файлов. Эти термины кажутся взаимозаменяемыми, но это не так:

  • Разделение пакетов: создает больше файлов меньшего размера для лучшего кэширования (но по-прежнему загружает файлы для каждого запроса).
  • Разделение кода: динамическая загрузка кода, поэтому пользователю нужно загрузить только ту часть кода, которую он в данный момент просматривает на сайте.

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

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

Давайте углубимся в понимание

Bundle VS Chunk VS Module

Прежде чем мы начнем программировать, нам еще нужно прояснить некоторые понятия. Например, «чанк», который мы используем на протяжении всего текста, и чем он отличается от «комплекта» и «модуля», на которые мы часто ссылаемся.

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

Прежде всего, я считаю, что у всех нет возражений против понятия «модуль», под которым понимаются фрагменты кода, которые мы сознательно инкапсулируем и организуем в процессе кодирования. В узком смысле мы сначала думаем о фрагментированных компонентах React, или модулях CommonJS, или модулях ES6, но для Webpack и Loader модули в широком смысле также включают стили и изображения и даже разные типы файлов.

«Пакет» — это отдельный файл, в который упакован весь связанный с ним код. Если вы не хотите помещать весь свой код в один пакет, вы можете разделить его на несколько пакетов или «чанков». С этой точки зрения «блоки» равны «пакетам», представляющим собой организацию и инкапсуляцию кода другим слоем. Если нам нужно сделать различие, обычно при обсуждении пакет относится к одному файлу, в который упакованы все модули, а фрагмент относится к набору модулей в соответствии с определенными правилами.Объем фрагмента больше, чем один модуль. и меньше, чем весь пакет

(Но если присмотреться,ChunkЭто технический термин, используемый Webpack для управления процессом упаковки, и даже может быть разделен на различные типы фрагментов. Я не думаю, что нам нужно понимать это с этой точки зрения. Просто запомните определение в предыдущем абзаце)

Разделение пакетов

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

Стоит отметить, что поскольку разделение пакетов связано с кешированием, для тех, кто впервые заходит на сайт, нет никакой разницы.

(Я думаю, что слишком много обсуждений производительности посвящено первому посещению сайта. Возможно, отчасти потому, что первое изображение важно, а отчасти потому, что эту часть производительности легко и аккуратно измерить)

Когда дело доходит до частых посетителей, немного сложнее количественно оценить прирост производительности, но мы должны это сделать!

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

Мы принимаем сценарий:

  • Алиса посещает сайт еженедельно в течение 10 недель подряд.
  • Мы обновляем сайт еженедельно
  • Мы обновляем страницу со списком продуктов еженедельно
  • У нас также есть страница сведений о продукте, но в настоящее время ее не нужно обновлять.
  • На пятой неделе мы добавили на сайт пакет npm.
  • На 8 неделе мы обновили существующий пакет npm.

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

базовый уровень производительности

Предполагая, что общий размер нашего пакета JavaScript составляет 400 КБ, назовите его какmain.js, затем загрузите его как один файл

У нас есть конфигурация Webpack, подобная следующей (я удалил лишние конфиги):

const path = require('path');

module.exports = {
  entry: path.resolve(__dirname, 'src/index.js'),
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
  },
};

Когда есть только одна запись, Webpack автоматически назовет результат какmain.js

(Для тех, кто плохо знаком с медлительностью, поясню: всякий раз, когда я упоминаюmain.jsкогда я на самом деле говорю что-то вродеmain.xMePWxHo.jsТакого рода вещи содержат кучу строк с хешами содержимого файла. Это означает, что при изменении кода вашего приложения генерируются новые имена файлов, что заставляет браузер загружать новый файл)

Поэтому каждую неделю, когда я публикую новые изменения на сайте, пакетcontenthashпроизойдут изменения. Настолько, что Алисе приходится каждую неделю загружать новый файл размером 400 КБ, когда она посещает наш сайт.

десять недель подрядЭто 4,12 МБ

мы можем сделать лучше

хэш и производительность

Я не знаю, действительно ли вы понимаете приведенное выше утверждение. Здесь необходимо уточнить несколько моментов:

  1. Почему имена файлов с хеш-строками влияют на кеширование браузера?
  2. почему суффикс хеш в имени файлаcontenthash? если поставитьcontenthashзаменитьhashилиchunkhashКаково влияние?

Чтобы предотвратить повторную загрузку браузером одного и того же файла при каждом посещении, мы обычно помещаем заголовок HTTP, возвращаемый этим файлом, вCache-ControlУстановить какmax-age=31536000, то есть год (в секундах). Таким образом, когда пользователь обращается к этому файлу в течение одного года, он не будет снова отправлять запрос на сервер, а будет читать его прямо из кеша до тех пор, пока не очистит кеш вручную.

Что, если я изменю содержимое файла на полпути и мне придется попросить пользователя загрузить его снова? Просто измените имя файла.Разные файлы (имена) соответствуют разным стратегиям кэширования. А хэш-строка — это «подпись», созданная на основе содержимого файла. Всякий раз, когда содержимое файла изменяется, хэш-строка также меняется, и соответственно изменяется имя файла. В результате политика кэширования старой версии файла будет признана недействительной, и браузер перезагрузит новую версию файла. Конечно, это только одна из самых основных стратегий кэширования, более сложные сценарии можно найти в моей предыдущей статье:Проектирование неуязвимого решения для кэширования браузера: идеи, детали, ServiceWorker и HTTP/2

Так настроено в Webpackfilename: [name]:[contenthash].jsПросто автоматически генерировать новое имя файла каждый раз, когда вы публикуете.

Однако, если вы немного знакомы с Webpack, вы должны знать, что Webpack также предоставляет два других алгоритма хеширования для использования разработчиками:hashа такжеchunkhash. Так почему бы не использовать их вместоcontenthash? Это начинается с их различий. В принципе, они служат разным целям, но на практике их также можно использовать взаимозаменяемо.

Для иллюстрации мы сначала подготовим следующую очень простую конфигурацию Webpack, которая имеет две записи упаковки, дополнительно извлекает файлы css и, наконец, создает три файла.filenameВ конфигурации мы используемhashидентификатор, вMinCssExtractPluginв том, что мы используемcontenthash, почему это происходит, будет объяснено позже.

const CleanWebpackPlugin = require("clean-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  entry: {
    module_a: "./src/module_a.js",
    module_b: "./src/module_b.js"
  },
  output: {
    filename: "[name].[hash].js"
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].[contenthash].css"
    })
  ]
};

hash

hashДля каждого билда (билда) хеши файлов, сгенерированные после каждого билда, согласованы. Он заботится об изменениях всего проекта, поскольку содержимое любого файла изменяется, хэши других файлов также будут меняться после сборки.

Очевидно, это не то, что нам нужно, еслиmodule_aСодержимое файла изменилось,module_aХэш упакованного файла должен измениться, ноmodule_bНе следует. Это заставит пользователя повторно загрузить неизмененныйmodule_bпакетный файл

chunkhash

chunkhashИсходя из изменения содержимого каждого чанка, если содержимое чанка изменилось, изменится только хеш выходного файла чанка, а остальные нет. Это похоже на то, что нам нужно.

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

  • contenthash

Как следует из названия, этот хэш основан на содержимом файла. С этой точки зрения, этоchunkhashвзаимозаменяемы. Итак, в коде «базового уровня производительности» автор использовалcontenthash

Но особенность в том, или в инструкции, которую я читал об этом, написано, что если вы хотитеExtractTextWebpackPluginилиMiniCssExtractPluginчтобы использовать хэш-идентификаторы, вы должны использоватьcontenthash. Но для моего личного тестирования, используяhashилиchunkhashТакже нет проблем (может быть, потому что плагин извлечения строго основан на содержании? Но разве это не фрагмент?)

Отдельная библиотека классов сторонней библиотеки (поставщика)

Разделим файл пакета наmain.jsа такжеvendor.js

Это так же просто, как:

const path = require('path');

module.exports = {
  entry: path.resolve(__dirname, 'src/index.js'),
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
    },
  },
};

Webpack 4 делает все возможное, чтобы сделать его лучше, не говоря вам, как вы хотите разделить пакет.

Это приводит к некоторым голосам, говорящим: «Удивительно, Webpack делает отличную работу!»

И другие голоса говорят: «Что ты сделал с моим пак-файлом!»

В любом случае, добавьтеoptimization.splitChunks.chunks = 'all'Конфигурация также говорит: "поставить всеnode_modulesположить все вvendors~main.jsфайл"

После реализации основных условий разделения пакетов Алисе по-прежнему необходимо загружать файл размером 200 КБ при каждом посещении.main.jsфайл, но нужно загрузить только 200 КБ в недели 1, 5 и 8vendors.jsсценарий

Это 2,64 МБ

Объем уменьшился на 36%. Неплохо для пяти новых строк кода, добавленных в конфиг. Вы можете попробовать это прямо сейчас, прежде чем читать дальше. Если вам нужно перейти с Webpack 3 на 4, не беспокойтесь, это безболезненно (и бесплатно!)

Отдельный каждый пакет NPM

нашvendors.jsтерпеть и начинатьmain.jsТа же проблема с файлами - частичная модификация будет означать повторную загрузку всех файлов

Так почему бы не разделить каждый пакет npm на отдельные файлы? очень просто сделать

давайте положим нашreact,lodash,redux,momentи т. д. разделены на разные файлы


const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: path.resolve(__dirname, 'src/index.js'),
  plugins: [
    new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly
  ],
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
  },
  optimization: {
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 0,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            // get the name. E.g. node_modules/packageName/not/this/part.js
            // or node_modules/packageName
            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];

            // npm package names are URL-safe, but some servers don't like @ symbols
            return `npm.${packageName.replace('@', '')}`;
          },
        },
      },
    },
  },
};

этот документОчень хорошее объяснение того, что здесь происходит, но мне все еще нужно объяснить тонкости этого, потому что мне потребовалось довольно много времени, чтобы понять

  • Webpack имеет некоторые не очень умные «умные» конфигурации по умолчанию, такие как разрешение только до 3 файлов при разделении связанных выходных файлов и минимальный размер файла 30 КБ (и объединение файлов меньшего размера, если они существуют). Поэтому я перезаписываю эти конфиги
  • cacheGroupsЗдесь мы создаем правила, которые сообщают Webpack, как фрагменты должны быть организованы в связанные выходные файлы. Я здесь для всей нагрузки отnode_modulesМодуль создает правило под названием «поставщик». Как правило, вам нужно только установить значение вашего выходного файла дляnameОпределите строку. но я поставилnameОпределяется как функция (вызывается при анализе файла). В функции я верну имя пакета на основе пути к модулю. В результате для каждого пакета я получаю отдельный файл, напримерnpm.react-dom.899sadfhj4.js
  • Чтобы можно было нормально публиковатьИмена пакетов npm должны быть допустимыми URL-адресами., поэтому нам не нужноencodeURIУскользающие существительные из пакета. Но у меня проблема в том, что сервер .NET не дает имена, содержащие@для обслуживания файла, поэтому я заменил его во фрагменте кода
  • После этого параметры конфигурации для всего шага не обслуживаются — нам не нужно обращаться к какой-либо библиотеке классов по имени.

Алиса повторно загружает 200 КБ еженедельноmain.jsфайл, и ей по-прежнему нужно загрузить файл пакета npm размером 200 КБ при первом посещении, но ей больше не нужно загружать один и тот же пакет дважды.

то есть2.24MB

Сокращение на 44% по сравнению с базовым уровнем, это действительно классный фрагмент кода, который вы можете вставить и скопировать из статьи.

Интересно, сможем ли мы выйти за пределы 50%?

разве это не было бы здорово

Подождите, что происходит с этим кодом конфигурации Webpack?

На данный момент у вас может возникнуть сомнение, как конфигурация в опции оптимизации отделяет код поставщика?

В следующем разделе будут описаны параметры оптимизации Webpack. Я не эксперт в Webpack, и конфигурация и соответствующие функции описания не были проверены один за другим, и не все они покрыты, Если есть какие-то ошибки, пожалуйста, простите меня.

optimizationКонфигурации, как следует из названия, созданы для оптимизации кода. Если присмотреться, большая часть конфигурации снова находится вsplitChunkПоле, потому что оно косвенно использует SplitChunkPlugin для реализации функции разделения чанков (это новые механизмы, введенные в Webpack 4. CommonsChunkPlugin используется в Webpack 3, который больше не используется в 4. Итак, здесь мы уделяем основное внимание настройке SplitChunkPlugin). ) В целом, SplitChunksPlugin имеет только одну функцию, котораяsplit- Разделите код. Разделение — это разделение одного большого файла на несколько файлов меньшего размера, а не упаковка всех модулей в один файл.

При первоначальном разделении кода вендора мы использовали только одну конфигурацию

splitChunks: {
  chunks: 'all',
},

chunksЕсть три варианта:initial,asyncа такжеall. Он указывает, следует ли предпочесть разделение синхронных (начальных), асинхронных (асинхронных) или всех модулей кода. Асинхронный здесь относится к методу динамической загрузки (import()) загруженные модули.

Дело в том, чтоприоритетДва слова. кasyncНапример, предположим, что у вас есть два модуля a и b, оба из которых ссылаются на jQuery, но модуль a также импортирует lodash посредством динамической загрузки. затем вasyncВ режиме плагин будет отделяться при упаковкеlodash~for~a.jsМодуль фрагментов a и b и общий модуль jQuery для a и b не будут (оптимизированы) разделены, поэтому они также могут существовать в упакованномa.bundle.jsа такжеb.bundle.jsв файле. потому чтоasyncСкажите плагину, чтобы он отдавал приоритет динамически загружаемым модулям.

Затем сосредоточьтесь на втором абзаце, разделяющем конфигурацию Webpack каждого пакета npm.

maxInitialRequestsа такжеminSizeЭто действительно шедевр самолюбия плагина. Плагин поставляется с некоторыми правилами разделения чанков: если размер файла чанка, который нужно разделить, меньше 30 КБ, чанк не будет разделен, а максимальное количество запросов на чанки, загружаемые параллельно, ограничено 3. покрываяminSizeа такжеmaxInitialRequestsКонфигурация может переопределить эти два параметра. Обратите внимание здесьmaxInitialRequestsа такжеminSizeвsplitChunksВ корневом каталоге давайте назовем это глобальную конфигурацию на данный момент

cacheGroupsКонфигурация является наиболее важной, что позволяет пользовательским правилам отделять CHUNK. И каждыйcacheGroupsПравилами разрешено определять вышеуказанныеchunksа такжеminSizeПоле используется для переопределения глобальной конфигурации (илиcacheGroupsв правилахenforceпараметр установлен наtrueигнорировать глобальную конфигурацию)

cacheGroupsидет по умолчаниюvendorsнастроить для разделенияnode_modulesВ модуле библиотеки классов его конфигурация по умолчанию выглядит следующим образом:

cacheGroups: {
  vendors: {
    test: /[\\/]node_modules[\\/]/,
    priority: -10
  },

Если вы не хотите использовать его конфигурацию, вы можете установить его какfalseИли переписать. Здесь я выбираю переписать и добавить дополнительную конфигурациюnameа такжеenforce:

vendors: {
  test: /[\\/]node_modules[\\/]/,
  name: 'vendors',
  enforce: true,
},

Наконец, представлены две конфигурации, которые не указаны выше, но все еще широко используются:priorityа такжеreuseExistingChunk

  • reuseExistingChunk: эта опция появится только вcacheGroupsВ правиле разделения это означает повторное использование существующего фрагмента. Например, чанк 1 владеет модулями A, B и C, а чанк 2 — модулями B и C. еслиreuseExistingChunkдляfalseВ случае упаковки плагин создаст для нас отдельный чанк с именемcommon~for~1~2, который содержит общие модули B и C. и если значениеtrue, т.к. в чанке 2 уже есть общедоступные модули B и C, плагин не будет создавать для нас новые модули

  • priority: Легко представить, что мы быcacheGroupsНастройте правила разделения нескольких фрагментов в файлах . Что, если один и тот же модуль соответствует нескольким правилам одновременно,priorityрешил эту проблему. Обратите внимание, что все настройки по умолчаниюpriorityвсе отрицательные числа, поэтому обычайpriorityДолжно быть больше или равно 0

резюме

До сих пор мы видели набор шаблонов для разделения кода:

  • Сначала решите, какую проблему мы хотим решить (чтобы пользователи не загружали дополнительный код при каждом посещении);
  • Затем решите, какое решение использовать (отделив код с низкой частотой изменений и повторений и сопоставив его с соответствующей стратегией кэширования);
  • Наконец решить, что реализовать (разделение кода путем настройки Webpack)

Эта статья также была опубликована в моемЗнай колонку, приветствую всех, чтобы обратить внимание

использованная литература

Bundle VS Chunk

Hash

SplitChunksPlugin