Руководство по оптимизации Webpack 4 Tree Shaking Ultimate

Webpack

Несколько месяцев назад мне поручили обновить конфигурацию сборки проекта Vue.js нашей группы до Webpack 4. Одной из наших главных целей было воспользоваться преимуществом tree-shaking, когда Webpack удаляет код, который на самом деле не используется, чтобы уменьшить размер пакета. Теперь преимущества встряхивания деревьев будут различаться в зависимости от вашей кодовой базы. Из-за нескольких наших архитектурных решений мы взяли много кода из других библиотек внутри компании и использовали лишь небольшую его часть.

Я пишу эту статью, потому что правильно оптимизировать Webpack непросто. Сначала я подумал, что это какое-то простое волшебство, но потом я провел месяц в Интернете в поисках ответов на ряд вопросов, которые у меня были. Я надеюсь, что с этим постом другим будет легче решать подобные проблемы.

Поговорим о преимуществах

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

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

В общем, после реализации tree-shaking наше приложение сократилось с 25% до 75% в зависимости от приложения. Средняя скорость сокращения в 52% в основном обусловлена ​​этими огромными общими библиотеками, которые являются основным кодом в небольших приложениях.

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

Нет репозитория примеров кода

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

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

что такое мертвый код

Просто: Webpack не видит код, который вы используете. Webpack отслеживает операторы импорта/экспорта для всего приложения, поэтому, если он видит, что что-то импортированное не используется в конце концов, он считает, что это «мертвый код» и будет встряхивать его.

Мертвый код не всегда так ясен. Ниже приведены несколько примеров мертвого кода и «живого» кода, которые, надеюсь, сделают его более понятным. Имейте в виду, что в некоторых случаях Webpack будет рассматривать что-то как мертвый код, даже если это не так. См. раздел «Побочные эффекты», чтобы узнать, как с этим бороться.

// 导入并赋值给 JavaScript 对象,然后在下面的代码中被用到
// 这会被看作“活”代码,不会做 tree-shaking
import Stuff from './stuff';
doSomething(Stuff);
// 导入并赋值给 JavaScript 对象,但在接下来的代码里没有用到
// 这就会被当做“死”代码,会被 tree-shaking
import Stuff from './stuff';
doSomething();
// 导入但没有赋值给 JavaScript 对象,也没有在代码里用到
// 这会被当做“死”代码,会被 tree-shaking
import './stuff';
doSomething();
// 导入整个库,但是没有赋值给 JavaScript 对象,也没有在代码里用到
// 非常奇怪,这竟然被当做“活”代码,因为 Webpack 对库的导入和本地代码导入的处理方式不同。
import 'my-lib';
doSomething();

Напишите импорт так, чтобы он поддерживал встряхивание дерева

То, как вы импортируете, очень важно при написании кода, который поддерживает встряхивание деревьев. Вам следует избегать импорта всей библиотеки в один объект JavaScript. Когда вы делаете это, вы говорите Webpack, что вам нужна вся библиотека, и Webpack не будет ее трясти.

Возьмем, к примеру, популярную библиотеку Lodash. Импорт всей библиотеки сразу — большая ошибка, но гораздо лучше импортировать отдельные модули. Конечно, Lodash требует дополнительных шагов для встряхивания дерева, но это хорошая отправная точка.

// 全部导入 (不支持 tree-shaking)
import _ from 'lodash';
// 具名导入(支持 tree-shaking)
import { debounce } from 'lodash';
// 直接导入具体的模块 (支持 tree-shaking)
import debounce from 'lodash/lib/debounce';

Базовая конфигурация веб-пакета

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

Во-первых, вы должны быть в рабочем режиме. Webpack выполняет встряску дерева только при минимизации кода, и это происходит только в рабочем режиме.

Во-вторых, для параметра оптимизации «usedExports» должно быть установлено значение true. Это означает, что Webpack идентифицирует код, который, по его мнению, не используется, и пометит его на начальном этапе объединения.

Наконец, вам нужно использовать компрессор, который поддерживает удаление мертвого кода. Этот минификатор распознает, как Webpack помечает код, который, по его мнению, не используется, и удаляет его. TerserPlugin поддерживает эту функцию и рекомендуется к использованию.

Ниже приведена базовая конфигурация Webpack для включения встряхивания дерева:

// Base Webpack Config for Tree Shaking
const config = {
 mode: 'production',
 optimization: {
  usedExports: true,
  minimizer: [
   new TerserPlugin({...})
  ]
 }
};

Какие побочные эффекты

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

Webpack считает такие файлы «побочными эффектами». Файлы с побочными эффектами не должны трястись деревьями, так как это нарушит работу всего приложения. Разработчики Webpack четко осознавали риск объединения кода, не зная, какие файлы имеют побочные эффекты, и поэтому по умолчанию рассматривали весь код как имеющий побочные эффекты. Это защитит вас от удаления необходимых файлов, но это означает, что поведение Webpack по умолчанию на самом деле не будет выполнять встряхивание дерева.

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

Как сообщить Webpack, что ваш код не содержит побочных эффектов

package.jsonимеет особое свойствоsideEffects, который существует для этого. Он имеет три возможных значения:

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

falseСкажите Webpack, что файлы не имеют побочных эффектов, все файлы качают дерево.

Третье значение[…]представляет собой массив путей к файлам. Он сообщает webpack, что ни один из ваших файлов не имеет побочных эффектов, кроме включенных в массив. Следовательно, все файлы, кроме указанных, безопасны для древовидной тряски.

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

НижеsideEffectsНесколько примеров кода для флагов. Несмотря на комментарии JavaScript, это код JSON:

// 所有文件都有副作用,全都不可 tree-shaking
{
 "sideEffects": true
}
// 没有文件有副作用,全都可以 tree-shaking
{
 "sideEffects": false
}
// 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
{
 "sideEffects": [
  "./src/file1.js",
  "./src/file2.js"
 ]
}

Глобальный CSS и побочные эффекты

Во-первых, давайте определим глобальный CSS в этом контексте. Глобальный CSS — это таблица стилей (может быть CSS, SCSS и т. д.), импортированная непосредственно в файл JavaScript. Он не преобразуется в модули CSS или что-то в этом роде. По сути, оператор импорта выглядит так:

// 导入全局 CSS
import './MyStylesheet.css';

Поэтому, если вы внесете побочные изменения, упомянутые выше, вы сразу же заметите сложную проблему при запуске сборки веб-пакета. Любые таблицы стилей, импортированные указанным выше способом, теперь будут удалены из вывода. Это связано с тем, что такие импорты рассматриваются webpack как мертвый код и удаляются.

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

Итак, чтобы сохранить глобальный файл CSS, нам просто нужно установить этот специальныйsideEffectsотмечен какtrue,так:

// 全局 CSS 副作用规则相关的 Webpack 配置
const config = {
 module: {
  rules: [
   {
    test: /regex/,
    use: [loaders],
    sideEffects: true
   }
  ]
 } 
};

Это свойство присутствует во всех модульных правилах Webpack. Правила, имеющие дело с глобальными таблицами стилей, должны использовать его, включая, помимо прочего, CSS/SCSS/LESS/и т. д.

Что такое модули и почему модули важны

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

Во-первых, нам нужно понять модули. С годами в JavaScript появилась возможность эффективного импорта/экспорта кода в «модулях» между файлами. Существует множество различных стандартов модулей JavaScript, которые существовали на протяжении многих лет, но для целей этой статьи мы сосредоточимся на двух. Один из них — «commonjs», а другой — «es2015». Вот их кодовая форма:

// Commonjs
const stuff = require('./stuff');
module.exports = stuff;

// es2015 
import stuff from './stuff';
export default stuff;

По умолчанию Babel предполагает, что мы пишем код с использованием модулей es2015, и транспилирует код JavaScript для использования модулей commonjs. Это сделано для широкой совместимости с серверными библиотеками JavaScript, которые обычно строятся поверх NodeJS (NodeJS поддерживает только модули commonjs). Тем не менее, вебпакне поддерживаетсяИспользуйте модуль commonjs для встряхивания дерева.

Теперь есть плагины (например, common-shake-plugin), которые утверждают, что дают Webpack возможность встряхивать модули commonjs, но, по моему опыту, они либо не работают, либо при работе с модулями es2015 они не работают. влияние тряски минимально. Я не рекомендую эти плагины.

Итак, чтобы выполнить встряску дерева, нам нужно скомпилировать код в модуль es2015.

Конфигурация модуля es2015 Babel

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

Итак, чтобы наш скомпилированный код мог использовать модули es2015, все, что нам нужно сделать, это попросить Babel оставить их в покое. Для этого нам просто нужно добавить следующее в наш babel.config.js (в этой статье вы увидите, что я предпочитаю конфигурацию JavaScript конфигурации JSON):

// es2015 模块的基本 Babel 配置
const config = {
 presets: [
  [
   '[@babel/preset-env](http://twitter.com/babel/preset-env)',
   {
    modules: false
   }
  ]
 ]
};

ПучокmodulesУстановить какfalse, который говорит Babel не компилировать код модуля. Это позволит Babel сохранить наши существующие операторы импорта/экспорта es2015.

**Важно!** Весь код, который может потребовать встряхивания дерева, должен быть скомпилирован таким образом. Поэтому, если у вас есть библиотеки для импорта, вы должны скомпилировать эти библиотеки как модули es2015 для встряхивания деревьев. Если они скомпилированы в commonjs, они не могут выполнять встряхивание деревьев и будут упакованы в ваше приложение. Многие библиотеки поддерживают частичный импорт, хорошим примером является lodash, это сам модуль commonjs, но у него есть версия lodash-es, которая использует модули es2015.

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

Извините, Jest бастует

Как и в других фреймворках для тестирования, мы используем Jest.

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

Причина такого результата проста: NodeJS. Jest разработан на основе NodeJS, а NodeJS не поддерживает модули es2015. Есть несколько способов настроить Node для этого, но это не работает с jest. Итак, мы застряли здесь: Webpack требует es2015 для встряхивания дерева, но Jest не может выполнять тесты на этих модулях.

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

Решение состоит из двух основных частей. Первая часть предназначена для кода самого проекта, то есть кода для запуска тестов. Эта часть проще. Вторая часть предназначена для кода библиотеки, то есть кода из других проектов, который компилируется в модули es2015 и импортируется в текущий проект. Эта часть более сложная.

Разрешить локальный код Jest проекта

Для нашей задачи у babel есть полезная функция: параметры окружения. Он может работать в разных средах через конфигурацию. Здесь нам нужны модули es2015 для сред разработки и производства, а также модули commonjs для тестовых сред. К счастью, Babel очень легко настроить:

// 分环境配置Babel 
const config = {
 env: {
  development: {
   presets: [
    [
     '[@babel/preset-env](http://twitter.com/babel/preset-env)',
     {
      modules: false
     }
    ]
   ]
  },
  production: {
   presets: [
    [
     '[@babel/preset-env](http://twitter.com/babel/preset-env)',
     {
      modules: false
     }
    ]
   ]
  },
  test: {
   presets: [
    [
     '[@babel/preset-env](http://twitter.com/babel/preset-env)',
     {
      modules: 'commonjs'
     }
    ]
   ],
   plugins: [
    'transform-es2015-modules-commonjs' // Not sure this is required, but I had added it anyway
   ]
  }
 }
};

После настройки весь локальный код проекта может нормально скомпилироваться, а также запустить тест Jest. Однако код сторонней библиотеки, использующий модули es2015, по-прежнему не работает.

Разрешение кода библиотеки в Jest

Причина, по которой код библиотек работает неправильно, очень очевиден, просто посмотрите на каталог Node_Module, чтобы понять. Код библиотеки здесь использует синтаксис модуля ES2015 для встряхивания деревьев. Библиотеки уже скомпилированы таким образом, поэтому, когда шутка пытается прочитать код в тесте подразделения, он дует вверх. Уведомление Нет, у нас уже есть Babel Включить модули Commonjs в тестовой среде, почему она не работает для этих библиотек? Это связано с тем, что jest (особенно Babel-jest) игнорирует любой код из Node_Modules по умолчанию при компиляции кода перед запуском тестов.

На самом деле это хорошо. Если Jest потребуется перекомпилировать все библиотеки, это значительно увеличит время обработки теста. Однако, хотя мы не хотим, чтобы он перекомпилировал весь код, мы хотим, чтобы он перекомпилировал библиотеки, использующие модули es2015, чтобы их можно было использовать в модульных тестах.

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

Настройте Jest для перекомпиляции кода библиотеки

// 重新编译库代码的 Jest 配置 
const path = require('path');
const librariesToRecompile = [
 'Library1',
 'Library2'
].join('|');
const config = {
 transformIgnorePatterns: [
  `[\\\/]node_modules[\\\/](?!(${librariesToRecompile})).*$`
 ],
 transform: {
  '^.+\.jsx?$': path.resolve(__dirname, 'transformer.js')
 }
};

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

transformIgnorePatternsэто функция конфигурации Jest, это массив обычных строк. Любой код, соответствующий этим регулярным выражениям, не будет перекомпилирован babel-jest. По умолчанию это строка "node_modules". Вот почему Jest не перекомпилирует какой-либо библиотечный код.

Когда мы предоставляем пользовательскую конфигурацию, как сказать «шуму» игнорировать код при пересылки. То есть зачем регулярное выражение, которое вы только что увидели, имеют отрицательное первое утверждение внутри, цель состоит в том, чтобы сопоставить весь код, отличный от библиотеки. Другими словами, мы скажем, Jest игнорировать весь код за пределами Node_Modules, кроме указанной библиотеки.

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

Вторая — это конфигурация преобразования, которая указывает на специальный преобразователь babel-jest. Я не уверен на 100%, что это обязательно, но все же добавил. Установите его для загрузки нашей конфигурации Babel, когда весь код будет перекомпилирован.

// Babel-Jest 转换器
const babelJest = require('babel-jest');
const path = require('path');
const cwd = process.cwd();
const babelConfig = require(path.resolve(cwd, 'babel.config'));
module.exports = babelJest.createTransformer(babelConfig);

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

Npm/Yarn Link — это дьявол

Далее идет еще одна проблема: связывание библиотек. Процесс связывания с помощью npm/yarn заключается в создании символической ссылки на локальный каталог проекта. Оказывается, Babel выдает много ошибок при перекомпиляции библиотек, связанных таким образом. Одна из причин, по которой мне потребовалось так много времени, чтобы понять эту штуку с Jest, заключается в том, что я связывал свою библиотеку таким образом и получил кучу ошибок.

Решение:не хочуИспользуйте ссылку npm/yarn. С такими инструментами, как «yalc», он может связывать локальные проекты, имитируя обычный процесс установки npm. Он не только не имеет проблем с перекомпиляцией Babel, но и лучше обрабатывает транзитивные зависимости.

Оптимизации для конкретных библиотек.

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

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

// 用 IgnorePlugin 移除多语言包
const { IgnorePlugin } from 'webpack';
const config = {
 plugins: [
  new IgnorePlugin(/^\.\/locale$/, /moment/)
 ]
};

Moment-Timezone — это старые часы MomentJS, а также большой парень. Его размер в основном вызван очень большим файлом JSON с информацией о часовом поясе. Я обнаружил, что просто сохранение годовых данных для этого века может уменьшить объем на 90%. Эта ситуация требует использования специального плагина Webpack.

// MomentTimezone Webpack Plugin
const MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin');
const config = {
 plugins: [
  new MomentTimezoneDataPlugin({
   startYear: 2018,
   endYear: 2100
  })
 ]
};

Lodash — еще один большой кусок раздутого пакета. К счастью, есть альтернативный пакет Lodash-es, который скомпилирован в модуль es2015 с флагом sideEffects. Замена Lodash на него может еще больше уменьшить размер пакета.

Кроме того, Lodash-Es, React-bootstrap и другие библиотеки могут добиться потери веса с помощью плагинов импорта Babel Transform. Плагин считывает оператор из библиотеки импорта файлов index.js и библиотеку, чтобы указывать на определенный файл. Это упростит библиотеки делать встряхивание дерева WebPack при разборке дерева модуля. В следующем примере показано, как это работает.

// Babel Transform Imports
// Babel config
const config = {
 plugins: [
  [
   'transform-imports',
   {
    'lodash-es': {
     transform: 'lodash/${member}',
     preventFullImport: true
    },
    'react-bootstrap': {
     transform: 'react-bootstrap/es/${member}', // The es folder contains es2015 module versions of the files
     preventFullImport: true
    }
   }
  ]
 ]
};
// 这些库不再支持全量导入,否则会报错
import _ from 'lodash-es';
// 具名导入依然支持
import { debounce } from 'loash-es';
// 不过这些具名导入会被babel编译成这样子
// import debounce from 'lodash-es/debounce';

Суммировать

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

оригинальный

общаться с

Добро пожаловать, чтобы отсканировать код и подписаться на общедоступную учетную запись WeChat «Станция перевода 1024», чтобы предоставить вам больше технических сухих товаров.

公众号:1024译站