Spec-Zone.ru › webpack 4

Встряска дерева

Встряска дерева — термин, часто используемый в контексте JavaScript для устранения неиспользуемого кода. Он опирается на статическую структуру синтаксиса модулей ES2015, то есть import и export. Название и концепция были популяризированы модульным бандлером ES2015 rollup.

Выпуск webpack 2 поставлялся со встроенной поддержкой модулей ES2015 (псевдоним модули harmony), а также с обнаружением неиспользуемых экспортов модулей. Новый выпуск webpack 4 расширяет эти возможности, предоставляя подсказки компилятору через свойство "sideEffects" package.json для обозначения файлов в вашем проекте, которые являются «чистыми» и поэтому безопасны для обрезки при отсутствии использования.

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

Добавить утилиту

Давайте добавим новый файл утилит в наш проект, src/math.js, который экспортирует две функции:

project

webpack-demo
|- package.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 не импортируется, однако он всё ещё включён в пакет. Мы исправим это в следующем разделе.

Отметить файл как свободный от побочных эффектов

В мире модулей ESM на 100%, определение побочных эффектов простое. Однако мы пока не там, поэтому, пока это необходимо, необходимо предоставить подсказки компилятору webpack о «чистоте» вашего кода.

Это достигается с помощью свойства "sideEffects" в файле package.json.

{
  "name": "your-project",
  "sideEffects": false
}

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

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

Однако, если в вашем коде есть побочные эффекты, вместо этого можно указать массив:

{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js"
  ]
}

Массив принимает относительные, абсолютные и шаблонные пути к соответствующим файлам. Он использует micromatch под капотом.

Обратите внимание, что любой импортированный файл подвержен встряске дерева. Это означает, что если вы используете что-то вроде css-loader в своём проекте и импортируете файл CSS, вам необходимо добавить его в список побочных эффектов, чтобы он не был случайно удалён в режиме производства:

{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js",
    "*.css"
  ]
}

Наконец, "sideEffects" также может быть установлено через опцию конфигурации module.rules.

Разъяснение встряски дерева и sideEffects

Оптимизации sideEffects и usedExports (более известные как встряска дерева) — это разные вещи.

sideEffects намного эффективнее, так как позволяет пропускать целые модули/файлы и всё поддерево.

usedExports полагается на terser для обнаружения побочных эффектов в операторах. Это сложная задача в JavaScript и не так эффективна, как прямой флаг sideEffects . Она также не может пропускать поддерево/зависимости, так как спецификация требует оценки побочных эффектов. Хотя экспорт функции работает нормально, HOC React в этом отношении являются проблематичными.

Давайте рассмотрим пример:

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"): «Если прямой экспорт из модуля, помеченного no-sideEffects, не используется, бандлер может пропустить оценку модуля на предмет побочных эффектов».

В примере Polaris Shopify исходные модули выглядят так:

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.js
  • components/Button.js
  • components/Button.css

После этой оптимизации могут применяться и другие оптимизации. Например: buttonFrom и buttonsFrom экспорты из Button.js также не используются. Оптимизация usedExports также подхватит это, и terser, возможно, сможет удалить некоторые операторы из модуля.

Также применяется конкатенация модулей. Таким образом, эти 4 модуля плюс модуль входа (и, вероятно, больше зависимостей) могут быть конкатенированы. index.js в итоге не генерирует никакого кода.

Сжать выходной результат

Итак, мы отметили наш «мёртвый код» для удаления, используя синтаксис 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',
};

Обратите внимание, что флаг --optimize-minimize можно использовать для включения TerserPlugin также.

С этим мы можем запустить ещё одну npm run build и посмотреть, изменилось ли что-нибудь.

Заметили ли вы какие-либо отличия в dist/bundle.js? Очевидно, весь пакет теперь сжат и замаскирован, но, если вы внимательно посмотрите, вы не увидите функцию square , но увидите замаскированную версию функции cube (function r(e){return e*e*e}n.a=r). Благодаря сжатию и встряске дерева, наш пакет теперь немного меньше! Хотя в этом искусственном примере это может показаться незначительным, встряска дерева может обеспечить значительное уменьшение размера пакета при работе с более крупными приложениями с сложными зависимостями.

ModuleConcatenationPlugin необходим для работы встряски дерева. Он добавляется mode: "production". Если вы его не используете, не забудьте добавить ModuleConcatenationPlugin вручную.

Заключение

Итак, чему мы научились, чтобы воспользоваться встряской дерева, необходимо…

  • Использовать синтаксис модулей ES2015 (то есть import и export).
  • Убедиться, что компиляторы не преобразуют ваш синтаксис модулей ES2015 в модули CommonJS (это поведение популярного пресета Babel @babel/preset-env — см. документацию для получения дополнительной информации).
  • Добавить свойство "sideEffects" в файл package.json вашего проекта.
  • Использовать production опцию конфигурации mode для включения различных оптимизаций, включая сжатие и встряску дерева.

Вы можете представить свое приложение как дерево. Источник кода и библиотеки, которые вы фактически используете, представляют собой зелёные, живые листья дерева. Мёртвый код — это бурые, сухие листья дерева, которые съедает осень. Чтобы избавиться от сухих листьев, нужно потрясти дерево, чтобы они упали.

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

Дополнительное чтение

  • webpack 4 beta — попробуйте сегодня!
  • Отладка срывов оптимизации
  • Задача 6074 — Добавить поддержку более сложных селекторов для sideEffects

© JS Foundation and other contributors
Licensed under the Creative Commons Attribution License 4.0.
https://v4.webpack.js.org/guides/tree-shaking

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API