Сжатие дерева
Сжатие дерева — термин, обычно используемый в контексте JavaScript для удаления неиспользуемого кода. Оно опирается на статическую структуру синтаксиса модулей ES2015, т. е. import и export. Название и концепция были популяризованы модульным бандлером ES2015 rollup.
Выпуск webpack 2 поставлялся с встроенной поддержкой модулей ES2015 (псевдоним модули harmony) а также обнаружения неиспользуемых экспортов модулей. Новый выпуск webpack 4 расширяет эти возможности, предоставляя способ подсказывать компилятору через "sideEffects" package.json свойство, чтобы обозначить, какие файлы в вашем проекте являются «чистыми» и, следовательно, безопасны для обрезки, если они не используются.
Добавить утилиту
Добавим новый файл утилиты в наш проект, src/math.js, который экспортирует две функции:
project
webpack-demo |- package.json |- package-lock.json |- webpack.config.js |- /dist |- bundle.js |- index.html |- /src |- index.js + |- math.js |- /node_modules
src/math.js
export function square(x) {
return x * x;
}
export function cube(x) {
return x * x * x;
} Установите mode параметр конфигурации на разработку, чтобы убедиться, что бандл не сжат:
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ mode: 'development',
+ optimization: {
+ usedExports: true,
+ },
}; После этого обновим наш скрипт входа, чтобы использовать один из этих новых методов и удалить lodash для простоты:
src/index.js
- import _ from 'lodash';
+ import { cube } from './math.js';
function component() {
- const element = document.createElement('div');
+ const element = document.createElement('pre');
- // Lodash, now imported by this script
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.innerHTML = [
+ 'Hello webpack!',
+ '5 cubed is equal to ' + cube(5)
+ ].join('\n\n');
return element;
}
document.body.appendChild(component()); Обратите внимание, что мы не import метод square из модуля src/math.js. Эта функция является тем, что известно как «мертвый код», то есть неиспользуемый export, который следует удалить. Теперь запустим наш скрипт npm, npm run build, и проанализируем выходной бандл:
dist/bundle.js (строки 90 - 100)
/* 1 */
/***/ (function (module, __webpack_exports__, __webpack_require__) {
'use strict';
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__['a'] = cube;
function square(x) {
return x * x;
}
function cube(x) {
return x * x * x;
}
}); Обратите внимание на unused harmony export square комментарий выше. Если вы посмотрите на код ниже него, вы заметите, что square не импортируется, однако он всё ещё включён в бандл. Мы исправим это в следующем разделе.
Пометить файл как свободный от побочных эффектов
В мире 100% модулей ESM определение побочных эффектов — тривиальная задача. Однако мы ещё не дошли до этого, поэтому на данный момент необходимо предоставить компилятору webpack подсказки о «чистоте» вашего кода.
Это достигается с помощью "sideEffects" свойства в файле package.json.
{
"name": "your-project",
"sideEffects": false
} Весь код выше не содержит побочных эффектов, поэтому мы можем установить значение свойства на false чтобы сообщить webpack, что он может безопасно обрезать неиспользуемые экспорты.
Однако, если в вашем коде есть побочные эффекты, вместо этого можно указать массив:
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js"]
} Массив принимает простые шаблоны glob для соответствующих файлов. Он использует glob-to-regexp под капотом (Поддерживает: *, **, {a,b}, [a-z]). Шаблоны, такие как *.css, не содержащие /, будут обрабатываться как **/*.css.
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
} Наконец, "sideEffects" также можно установить из module.rules параметра конфигурации.
Разъяснение сжатия дерева и sideEffects
Оптимизации sideEffects и usedExports (более известные как сжатие дерева) — это разные вещи.
sideEffects намного эффективнее, так как позволяет пропустить целые модули/файлы и всё поддерево.
usedExports полагается на terser для обнаружения побочных эффектов в выражениях. Это сложная задача в JavaScript и не так эффективна, как прямой sideEffects флаг. Она также не может пропустить поддерево/зависимости, так как спецификация гласит, что побочные эффекты должны оцениваться. Хотя экспорт функций работает нормально, компоненты React Higher Order (HOC) представляют проблемы в этом отношении.
Давайте рассмотрим пример:
import { Button } from '@shopify/polaris'; Предварительно собранная версия выглядит так:
import hoistStatics from 'hoist-non-react-statics';
function Button(_ref) {
// ...
}
function merge() {
var _final = {};
for (
var _len = arguments.length, objs = new Array(_len), _key = 0;
_key < _len;
_key++
) {
objs[_key] = arguments[_key];
}
for (var _i = 0, _objs = objs; _i < _objs.length; _i++) {
var obj = _objs[_i];
mergeRecursively(_final, obj);
}
return _final;
}
function withAppProvider() {
return function addProvider(WrappedComponent) {
var WithProvider =
/*#__PURE__*/
(function (_React$Component) {
// ...
return WithProvider;
})(Component);
WithProvider.contextTypes = WrappedComponent.contextTypes
? merge(WrappedComponent.contextTypes, polarisAppProviderContextTypes)
: polarisAppProviderContextTypes;
var FinalComponent = hoistStatics(WithProvider, WrappedComponent);
return FinalComponent;
};
}
var Button$1 = withAppProvider()(Button);
export {
// ...,
Button$1,
}; Когда Button не используется, вы можете эффективно удалить export { Button$1 };, что оставляет весь оставшийся код. И вопрос в том, «имеет ли этот код какие-либо побочные эффекты или его можно безопасно удалить?». Сложно сказать, особенно из-за этой строки withAppProvider()(Button). withAppProvider вызывается, а значение возврата также вызывается. Являются ли вызовы merge или hoistStatics побочными эффектами? Существуют ли побочные эффекты при назначении WithProvider.contextTypes (setter?) или при чтении WrappedComponent.contextTypes (getter)?
Terser пытается это выяснить, но во многих случаях не может этого сделать наверняка. Это не означает, что terser не справляется со своей работой, поскольку не может это выяснить. Слишком сложно надёжно определить это в динамическом языке, таком как JavaScript.
Но мы можем помочь terser, используя аннотацию /*#__PURE__*/. Она помечает выражение как свободное от побочных эффектов. Небольшая модификация позволит сжать код:
var Button$1 = /*#__PURE__*/ withAppProvider()(Button);
Это позволит удалить этот фрагмент кода. Но всё ещё остаются вопросы с импортами, которые необходимо включить/оценить, так как они могут содержать побочные эффекты.
Для решения этой проблемы мы используем свойство "sideEffects" в файле package.json.
Это аналогично /*#__PURE__*/, но на уровне модуля, а не выражения. Это говорит (свойство "sideEffects"): «Если прямой экспорт из модуля, помеченного как без побочных эффектов, не используется, бандлер может пропустить оценку модуля на наличие побочных эффектов».
В примере Shopify's Polaris исходные модули выглядят так:
index.js
import './configure'; export * from './types'; export * from './components';
components/index.js
// ...
export { default as Breadcrumbs } from './Breadcrumbs';
export { default as Button, buttonFrom, buttonsFrom } from './Button';
export { default as ButtonGroup } from './ButtonGroup';
// ... package.json
// ... "sideEffects": [ "**/*.css", "**/*.scss", "./esnext/index.js", "./esnext/configure.js" ], // ...
Для import { Button } from "@shopify/polaris"; это имеет следующие последствия:
- включить: включить модуль, оценить его и продолжить анализ зависимостей
- пропустить: не включать, не оценивать, но продолжить анализ зависимостей
- исключить: не включать, не оценивать и не анализировать зависимости
Конкретно для соответствующих ресурса(ов):
-
index.js: Прямой экспорт не используется, но помечен как sideEffects —> включить -
configure.js: Экспорт не используется, но помечен как sideEffects —> включить -
types/index.js: Экспорт не используется, не помечен как sideEffects —> исключить -
components/index.js: Прямой экспорт не используется, не помечен как sideEffects, но используются экспорты, переэкспортированные из него —> пропустить -
components/Breadcrumbs.js: Экспорт не используется, не помечен как sideEffects —> исключить. Это также исключает все зависимости, такие какcomponents/Breadcrumbs.css, даже если они помечены как sideEffects. -
components/Button.js: Прямой экспорт используется, не помечен как sideEffects —> включить -
components/Button.css: Экспорт не используется, но помечен как sideEffects —> включить
В этом случае только 4 модуля включены в бандл:
-
index.js: практически пустой configure.jscomponents/Button.jscomponents/Button.css
После этой оптимизации могут применяться и другие оптимизации. Например: buttonFrom и buttonsFrom экспорты из Button.js также не используются. Оптимизация usedExports подберёт их, и terser, возможно, сможет удалить некоторые выражения из модуля.
Также применяется конкатенация модулей. Таким образом, эти 4 модуля плюс модуль входа (и, вероятно, больше зависимостей) могут быть сконкатенированы. index.js в конечном итоге не генерирует код.
Помечать вызовы функций как свободные от побочных эффектов
Можно сказать webpack, что вызов функции свободен от побочных эффектов (чистый), используя аннотацию /*#__PURE__*/. Она может быть помещена перед вызовами функций, чтобы отметить их как свободные от побочных эффектов. Аргументы, передаваемые в функцию, не помечаются аннотацией и могут потребовать отдельной пометки. Когда начальное значение в объявлении переменной неиспользуемой переменной рассматривается как свободное от побочных эффектов (чистое), оно помечается как мёртвый код, не выполняется и удаляется минимизатором. Это поведение включено, когда optimization.innerGraph установлено на true.
file.js
/*#__PURE__*/ double(55);
Минифицировать вывод
Итак, мы подготовили наш «мёртвый код» для удаления с помощью синтаксиса import и export, но нам всё ещё нужно удалить его из бандла. Для этого установите mode параметр конфигурации на production.
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
- mode: 'development',
- optimization: {
- usedExports: true,
- }
+ mode: 'production',
}; С этим можно выполнить ещё один npm run build и посмотреть, изменилось ли что-то.
Заметили ли вы что-нибудь другое в dist/bundle.js? Весь бандл теперь сжат и искажён, но, если присмотреться, вы не увидите функцию square, а увидите искажённую версию функции cube (function r(e){return e*e*e}n.a=r). Благодаря минимизации и сжатию дерева наш бандл стал на несколько байт меньше! Хотя это может показаться незначительным в этом искусственном примере, сжатие дерева может привести к значительному уменьшению размера бандла при работе с более крупными приложениями с сложными деревьями зависимостей.
Заключение
Мы узнали, что для использования сжатия дерева необходимо...
- Используйте синтаксис модулей ES2015 (например,
importиexport). - Убедитесь, что компиляторы не преобразуют синтаксис модулей ES2015 в модули CommonJS (это поведение по умолчанию популярного пресета Babel @babel/preset-env — см. документацию для получения более подробной информации).
- Добавьте свойство
"sideEffects"в файлpackage.jsonвашего проекта. - Используйте параметр конфигурации
productionmode, чтобы включить различные оптимизации, включая минификацию и трейш-шейкинг (оптимизация побочных эффектов включена в режиме разработки с помощью значения флага). - Убедитесь, что вы установили правильное значение для
devtool, так как некоторые из них нельзя использовать в режимеproduction.
Вы можете представить свое приложение в виде дерева. Исходный код и библиотеки, которые вы фактически используете, представляют собой зелёные, живые листья дерева. Мёртвый код — это коричневые, отмершие листья дерева, которые съедает осень. Чтобы избавиться от этих отмерших листьев, нужно потрясти дерево, чтобы они упали.
Если вас интересуют другие способы оптимизации выходных данных, перейдите к следующему руководству для получения подробной информации о создании для производства.
© JS Foundation and other contributors
Licensed under the Creative Commons Attribution License 4.0.
https://webpack.js.org/guides/tree-shaking