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

JavaScript Webpack

предисловие

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

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

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

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

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

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


отдельный код приложения

Теперь давайте обратим внимание на то, что Алиса скачивала снова и снова.main.jsдокумент

Я упоминал ранее, что у нас есть две совершенно разные части нашего сайта: страница со списком продуктов и страница с подробной информацией. Отдельные упоминания кода на странице составляют около 25 КБ (общий код — 150 КБ).

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

Знаете ли вы, что у нас также есть SVG-файл размером 25 КБ, который отображает значок с небольшими изменениями? что нам с этим делать

Мы вручную добавляем несколько записей и говорим Webpack создать для них отдельные файлы:

module.exports = {
  entry: {
    main: path.resolve(__dirname, 'src/index.js'),
    ProductList: path.resolve(__dirname, 'src/ProductList/ProductList.js'),
    ProductPage: path.resolve(__dirname, 'src/ProductPage/ProductPage.js'),
    Icon: path.resolve(__dirname, 'src/Icon/Icon.js'),
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash:8].js',
  },
  plugins: [
    new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly
  ],
  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 автоматически создает отдельные файлы для общего между ними кода, т.е.ProductListа такжеProductPageнет повторяющегося кода

На этот раз Алиса экономит 50 КБ загрузок почти каждую неделю.

Осталось всего 1,815 МБ

Мы сохранили Алисе 56% загрузок и продолжим (в нашем теоретическом сценарии)

Все это достигается изменением конфигурации Webapck — мы не изменили ни одной строчки кода приложения.

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


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

Станет ли он медленнее, когда будет больше сетевых запросов?

Ответ очень ясен нет

Это верно для HTTP/1.1, но не для HTTP/2.

несмотря на это,Эта статья от 2016 годаи изАкадемия Хана 2015 СтатьиОба пришли к выводу, что даже с HTTP/2 загрузка слишком большого количества файлов все равно приведет к замедлению работы. Но в обеих статьях «слишком много» означает сотни файлов. Так что просто помните, что если у вас есть сотни файлов, вы можете достичь предела параллелизма.

Если вам интересно, как поддерживать HTTP/2 в IE11 в Windows 10. Я провел опрос тех, кто все еще использует старые машины, и они были на удивление последовательны и заверили меня, что их вообще не волнует скорость загрузки сайта.

Будет ли избыточный код шаблона в каждом упакованном файле webpack?

немного

Но что такое «код шаблона»?

Представьте, если бы весь проект состоял только из файловapp.js, то окончательный выходной файл пакета будет простоapp.jsтолько содержимое файла.

но еслиapp.jsЕсли содержимое файла пустое (без строки кода), то является ли окончательный упакованный файл пустым?

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

Если у меня есть несколько небольших файлов, уменьшается ли эффект сжатия?

да

Это факт:

  • = Multi-multi-файл шаблон шаблона Webpack
  • Несколько файлов = меньшее сжатие

Давайте проясним, каковы потери

Я только что сделал тест, и файл сайта размером 190 КБ был разделен на 19 файлов, примерно на 2% больше байтов, отправленных в браузер.

Итак... Увеличение количества упоминаний файла на 2% при первом посещении, но уменьшение размера файла на 60% при каждом последующем посещении до конца света.

Таким образом, правильное число для потери: нисколько.

Когда я тестировал 1 файл против 19 файлов, я подумал, что должен протестировать несколько разных сетевых сред, включая HTTP/1.1.

Ниже эта форма дает «чем больше ты», тем лучше »

Сокращение времени загрузки на 30 % при наличии 19 файлов в сетях 3G и 4G.

Но так ли это на самом деле?

Эти данные выглядят «зашумленными», например, при втором запуске в сети 4G сайт загружался 646 мс, а затем второй запуск занял 1116 мс — увеличение времени на 73%. Так что утверждение, что HTTP/2 на 30% быстрее, немного виновато.

Я создал эту таблицу, чтобы попытаться количественно определить, насколько сильно HTTP/2 действительно отличается, но единственное, что я могу сказать, это «не большая разница».

Настоящим сюрпризом стали последние две строки, более старые версии Windows и HTTP/1.1, как я ожидал, будут значительно медленнее. Думаю, мне нужна более медленная сетевая среда для аутентификации.


Время историй! Я извеб-сайт МайкрософтСкачал виртуальную машину Windows 7, чтобы проверить эти вещи

Я хочу обновить IE8 по умолчанию до IE9

Итак, я зашел на страницу Microsoft, чтобы загрузить IE9, и нашел:

И последнее слово о HTTP/2. Знаете ли вы, что он интегрирован в Node? Если хочешь попробовать, яНапишите службу HTTP/2 в 100 строк, что может помочь с кэшированием ваших тестов


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

Разделение кода (не нужно загружать код, который вам не нужен)

Эта конкретная практика действительна только для определенных сайтов.

Я рад повторить теорию 20/20, которую я изобрел: если есть части вашего сайта, которые посещают только 20% пользователей, и эта часть скрипта занимает более 20% всего вашего сайта, вам следует подумать о загрузке кода. на лету

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

как решить

Предположим, у вас есть торговый сайт, и вы задаетесь вопросом, следует ли вам выделить код для функции «оформления заказа», так как туда попадет только 30% пользователей.

Во-первых, лучше продавать

Во-вторых, выясните, сколько отдельных кодов существует для функции «оформить заказ». Поскольку вы часто выполняете «разделение файла пакета» перед «разделением кода», вы, возможно, уже знаете, сколько кода находится в этой части.

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

Предположим, вы заметили, что один код на странице оформления заказа всего 7 КБ, а код в других частях — 300 КБ. Увидев эту ситуацию, я бы предложил не разделять эти коды по нескольким причинам.

  • Это не замедляет загрузку. Помните, что вы загружали эти файлы параллельно раньше, вы можете попробовать записать, есть ли изменения в загрузке файлов 300 КБ и 307 КБ.
  • Если вы задержите загрузку этой части кода, пользователю все равно нужно дождаться загрузки файла после нажатия «оплатить» — вы же не хотите оказывать какое-либо сопротивление пользователю в этот критический момент.
  • Разделение кода приведет к изменениям в программном коде, что потребует смены ранее синхронной логики на асинхронную. Это не сложно, но все же слишком сложно для улучшения взаимодействия с пользователем по соотношению цена/производительность.

Вот что я имею в виду под фразой «эта захватывающая технология может быть не для вас».

Давайте рассмотрим два примера разделения кода.

Схема отката (Polyfills)

Мы начали с этого примера, потому что он работает для большинства сайтов и является очень хорошим стартером.

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

require('whatwg-fetch');
require('intl');
require('url-polyfill');
require('core-js/web/dom-collections');
require('core-js/es6/map');
require('core-js/es6/string');
require('core-js/es6/array');
require('core-js/es6/object');

Я был в моем входном файлеindex.jsЭтот файл импортируется вверху

import './polyfills';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App/App';
import './index.css';

const render = () => {
  ReactDOM.render(<App />, document.getElementById('root'));
}

render(); // yes I am pointless, for now

В разделе конфигурации Webpack, посвященном разделению пакетов, мой код отката автоматически разбивается на четыре разных файла, потому что существует четыре пакета npm. Они имеют размер около 25 КБ, и 90% браузеров не нуждаются в них, поэтому их стоит загружать динамически.

в Webpack 4 иimport()синтаксис (не иimportКод условной загрузки и отката становится очень простым благодаря поддержке синтаксической путаницы)

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App/App';
import './index.css';

const render = () => {
  ReactDOM.render(<App />, document.getElementById('root'));
}

if (
  'fetch' in window &&
  'Intl' in window &&
  'URL' in window &&
  'Map' in window &&
  'forEach' in NodeList.prototype &&
  'startsWith' in String.prototype &&
  'endsWith' in String.prototype &&
  'includes' in String.prototype &&
  'includes' in Array.prototype &&
  'assign' in Object &&
  'entries' in Object &&
  'keys' in Object
) {
  render();
} else {
  import('./polyfills').then(render);
}

Теперь это имеет больше смысла? Если браузер поддерживает все новые функции, то визуализируйте страницу. В противном случае загрузите код отката для отображения страницы. Когда код выполняется в браузере, среда выполнения Webpack позаботится о загрузке этих четырех пакетов, а когда они будут загружены и проанализированы,render()функция вызывается, а другая работа продолжает выполняться

(Кстати, если вам нужно использоватьimport()Если тебе нужноплагин динамического импорта для Babel. И, как объясняется в документации Webpack,import()Используйте обещания, поэтому вам нужно изолировать эту часть кода отката)

Очень просто, не так ли?

Есть более сложные сценарии

Динамическая загрузка на основе маршрута (для React)

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

Эта страница имеет множество полезных функций, множество диаграмм и требует установки библиотеки форм из npm. Поскольку я реализовал разделение кода упаковки, по крайней мере 100 КБ файлов были сохранены визуально.

Теперь я настраиваю копию, когда пользователь получает к ней доступ/adminпри рендеринге<AdminPage>маршрут. Когда Webpack все упакует, он будет искатьimport AdminPage from './AdminPage.js'И говорят: «Эй, мне нужно включить это в файл init Load»

Но мы не хотим этого делать, мы хотим загрузить страницу администратора в динамической загрузке, напримерimport('./AdminPage.js'), чтобы Webpack знал, что ему нужно загрузить его динамически.

очень круто, не требует настройки

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

Основная идея очень проста, когда компонент загружается (что означает, что пользователь посещает/adminвремя), мы динамически загружаем./AdminPage.jsа затем сохраните ссылку на него в состоянии компонента

В функции рендеринга во время ожидания<AdminPage> В процессе загрузки мы просто визуализируем<div>Loading...</div>, после успешной загрузки<AdminPage>

Для удовольствия я хотел бы реализовать это сам, но в реальном мире вам просто нужно сделать что-то вродеДокументация React по разделению кодаиспользуйте, как описаноreact-loadableТолько что


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

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

Спасибо за чтение и хорошего дня

Черт, я забыл упомянуть CSS

Об опыте разработки

Выше мы все разделяем код для производства. Но на самом деле мы сталкиваемся с той же проблемой во время разработки: когда увеличивается объем кода, увеличивается и время упаковки. Но, например, код в node_modules остается неизменным тысячи лет и его вообще не нужно перекомпилировать. В этой части мы также можем разделить код с помощью идеи разделения кода. например технология DLL

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

Использование DLL — это просто двухэтапный процесс:

Экспорт DLL-файла

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

module.exports = {
   entry: {
      library: [
         'react',
         'redux',
         'jquery',
         'd3',
         'highcharts',
         'bootstrap',
         'angular'
      ]
   },
   output: {
      filename: '[name].dll.js',
      path: path.resolve(__dirname, './build/library'),
      library: '[name]'
   },
   plugins: [
    new webpack.DllPlugin({
        name: '[name]',
        path: './build/library/[name].json'
    })
  ]
};

Ключ в том, чтобы использовать файл json, выводимый DLLPlugin, чтобы сообщить веб-пакету, где найти предварительно скомпилированный код библиотеки.

Импорт файлов DLL

Внесите DLL в конфигурацию Webpack, которая формально упаковывает скрипт приложения:

plugins: [
  new webpack.DllReferencePlugin({
    context: __dirname,
    manifest: require('./build/library/library.json')
  })
]

Но ложка дегтя в том, что вам все равно нужно импортировать файл dll на вашу последнюю страницу.

Если вы чувствуете, что ручная настройка dll по-прежнему кажется громоздкой, вы можете попробовать использоватьAutoDllPlugin

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

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

Bundle VS Chunk

Hash

SplitChunksPlugin

DLL