Ваш Tree-Shaking бесполезен

JavaScript Webpack rollup.js

В этой статье будет обсуждаться текущий статус встряхивания деревьев (webpack@3, babel@6 и ниже), а также причины, по которым встряхивание деревьев все еще борется, и, наконец, кратко изложены некоторые методы, которые могут улучшить эффект встряхивания деревьев. .

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

Tree-Shaking используется во внешнем интерфейсеrollupСначала предложили и внедрили, а затемwebpackВ версии 2.x также с помощьюUglifyJSДостигнуто. С тех пор Tree-Shaking можно увидеть в различных статьях, посвященных оптимизированной упаковке.

Многие разработчики очень радуются, когда видят это, думая, что более половины библиотек, таких как elementUI и antd, на которые они ссылаются, наконец-то могут быть удалены. Однако идеала полно, а реальность – это костяк. После обновления сжатый пакет проекта существенно не изменился.

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

Позвольте мне поделиться с вами моим процессом нащупывания Tree-Shaking.

Принцип Tree-Shaking

Я не буду подробно останавливаться на этом и сразу опубликую статью о внешнем интерфейсе Baidu Takeaway:Практика оптимизации производительности Tree-Shaking — принципы.

Если вам лень читать статью, вы можете прочитать следующее резюме:

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

Очень хорошо, принцип идеален, так почему наш код нельзя удалить?

Давайте сначала поговорим о причине: это все побочные эффекты!

побочный эффект

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

Например, такой как эта функция:

function go (url) {
  window.location.href = url
}

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

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

// componetns.js
export class Person {
  constructor ({ name, age, sex }) {
    this.className = 'Person'
    this.name = name
    this.age = age
    this.sex = sex
  }
  getName () {
    return this.name
  }
}
export class Apple {
  constructor ({ model }) {
    this.className = 'Apple'
    this.model = model
  }
  getModel () {
    return this.model
  }
}
// main.js
import { Apple } from './components'

const appleModel = new Apple({
  model: 'IphoneX'
}).getModel()

console.log(appleModel)

Пытался встряхнуть дерево с помощью онлайн-реплика rollup и действительно удалил Person,портал

Но почему нет способа удалить неиспользуемый код, когда я упаковываю библиотеку компонентов через веб-пакет и импортирую ее другими?

Потому что я упустил из виду две вещи: компиляцию babel + упаковку webpack

Успех — это тоже Вавилон, неудача — это тоже Вавилон.

Babel не нуждается в дополнительных пояснениях, он может преобразовывать код ES6/ES7 в код, который может поддерживать указанный браузер. Именно благодаря этому мы, фронтенд-разработчики, можем иметь такую ​​прекрасную среду разработки, как сегодня, и можем использовать новейшие функции языка JavaScript, не принимая во внимание совместимость с браузером.

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

Например, в моем примере выше, если мы используем babel для его компиляции, а затем вставляем в repl of rollup, результат будет следующим:портал

Если не утруждать себя открытием ссылки, то можете посмотреть на класс Person — результат компиляции babel:

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var _createClass = function() {
  function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      descriptor.enumerable = descriptor.enumerable || !1, descriptor.configurable = !0,
      "value" in descriptor && (descriptor.writable = !0), Object.defineProperty(target, descriptor.key, descriptor);
    }
  }
  return function(Constructor, protoProps, staticProps) {
    return protoProps && defineProperties(Constructor.prototype, protoProps), staticProps && defineProperties(Constructor, staticProps),
    Constructor;
  };
}()

var Person = function () {
  function Person(_ref) {
    var name = _ref.name, age = _ref.age, sex = _ref.sex;
    _classCallCheck(this, Person);

    this.className = 'Person';
    this.name = name;
    this.age = age;
    this.sex = sex;
  }

  _createClass(Person, [{
    key: 'getName',
    value: function getName() {
      return this.name;
    }
  }]);
  return Person;
}();

Наш класс Person инкапсулирован как IIFE (немедленно выполняемая функция), а затем возвращает конструктор. Так почему же у него есть побочные эффекты? Проблема в методе _createClass, нужно только изменить IIFE Person в ссылке repl предыдущего роллапа._createClassВызовите delete, класс Person будет удален. Что касается_createClassПочему есть побочные эффекты, давайте пока отложим это в сторону. Потому что у вас может возникнуть другой вопрос:Почему Babel объявляет конструкторы таким образом?

Если бы это был я, я мог бы скомпилировать так:

var Person = function () {
  function Person() {

  }
  Person.prototype.getName = function () { return this.name };
  return Person;
}();

Поскольку мы привыкли писать «класс» именно так, почему Babel использует его?Object.definePropertyЧто плохого в использовании цепочки прототипов в таком виде? Естественно, это очень неуместно, потому что некоторый синтаксис ES6 имеет свою специфическую семантику. Например:

  1. Методы, объявленные внутри класса, не являются перечисляемыми, в то время как методы, объявленные в цепочке прототипов, являются перечисляемыми. Здесь вы можете обратиться к вступлению г-на Руана.Основной синтаксис класса
  2. for...ofЦикл проходит через итератор (Iterator) итеративный, а не i++ при переборе массива в цикле, а затем найти значение по индексу. Здесь вы все еще можете увидеть введение г-на Руана в traverser и for...of, а также бабельную статью оfor...ofИнструкции по компиляцииtransform-es2015-for-of

Поэтому, чтобы соответствовать истинной семантике ES6, Babel берет на себяObject.definePropertyЧтобы определить метод прототипа, который приводит к последующей серии проблем.

Зоркие одноклассники могут разместить ссылку в моем втором пункте вышеtransform-es2015-for-ofКак видите, у Babel на самом деле естьlooseРежим в дословном переводе называется свободным режимом. Для чего это? Он не будет строго следовать семантике ES6 и примет привычку компилировать код, который больше соответствует нашему обычному написанию кода. такие как вышеPersonМетоды свойств класса будут скомпилированы в методы, объявленные непосредственно в цепочке прототипов.

Конкретная конфигурация Babel для этого режима выглядит следующим образом:

// .babelrc
{
  "presets": [["env", { "loose": false }]]
}

Точно так же я помещаю пример онлайн-репла, чтобы вы могли непосредственно увидеть эффект:loose-mode

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

мы включилиlooseмод, упакованный роллапом, и обнаружил, что это правда!портал

UglifyJS недостаточно

Но не радуйтесь, когда мы используем Webpack для упаковки файлов с помощью UglifyJS, IIFE этого класса Person снова упаковывается? какие? ? ?

Чтобы полностью понять эту проблему, я нашел проблему UglifyJS:Class declaration in IIFE considered as side effect, долго смотрел внимательно. Студенты, которые заинтересованы в этом и чей английский еще в порядке, могут быстро узнать об этом вопросе, что довольно интересно. Кратко поясню, что говорится по этому вопросу.

владелец выпуска-blacksonicЛюбопытно, почему UglifyJS не удаляет классы, на которые нет ссылок.

Авторы UglifyJS -kzcТем не менее, uglify не выполняет анализ хода выполнения программы, поэтому нельзя исключать код, который может иметь побочные эффекты.

Арендодатель: Мой код не имеет побочных эффектов. В противном случае можно придумать пункт конфигурации.Настроив его, можно подумать, что у него нет побочных эффектов, и потом смело их удалять.

Участник: У нас нет анализа потока программы, поэтому мы не можем этого сделать. Если мы действительно хотим их удалить, выйдите, поверните налево и сверните.

Арендодатель: Стоимость переноса накопительного пакета несколько высока. Я не думаю, что сложно добавить такую ​​конфигурацию, Барабара.

Авторы: PR приветствуются.

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

Участник: присваивание переменной может иметь побочные эффекты! привожу пример:

var V8Engine = (function () {
  function V8Engine () {}
  V8Engine.prototype.toString = function () { return 'V8' }
  return V8Engine
}())
var V6Engine = (function () {
  function V6Engine () {}
  V6Engine.prototype = V8Engine.prototype // <---- side effect
  V6Engine.prototype.toString = function () { return 'V6' }
  return V6Engine
}())
console.log(new V8Engine().toString())

Автор:V6Engine虽然没有被使用,但是它修改了V8Engine原型链上的属性,这就产生副作用了。 Смотретьrollup(Арендодатель конкретно указывает, что на данный момент) В настоящее время такая стратегия, удаление v6Engine напрямую, на самом деле правильная.

Хозяин и несколько прохожих, А, Б, В, Г, один за другим выдвигали свои предложения и планы. Окончательное решение может быть принято по коду/*@__PURE__*/Такая аннотация заявляет, что эта функция не имеет побочных эффектов.

Эта проблема содержит большое количество информации и является довольно интересной.Один из участников uglify, kzc, поднял вопрос о свертывании после того, как в то время поднял проблему свертки.В Rollup посчитали, что проблема незначительна и не срочная, и участник также подняли вопрос в роллап, сделали PR и проблема была решена. . .

Позвольте мне обобщить некоторые ключевые сведения из этого выпуска:

  1. Если параметр функции является ссылочным типом, работа его свойств может иметь побочные эффекты. Поскольку это в первую очередь ссылочный тип, любое изменение его свойств фактически изменяет данные вне функции. Во-вторых, получение или изменение его свойств вызоветgetterилиsetter,а такжеgetter,setterнепрозрачный и может иметь побочные эффекты.
  2. uglify не имеет сложного анализа потока программы. Он может просто определить, ссылаются ли на переменную и модифицируют ли ее впоследствии, но он не может судить о полном процессе модификации переменной, и он не знает, указывает ли он на внешнюю переменную, так что многие коды, которые могут иметь побочные эффекты, могут быть только консервативно не удалял.
  3. Сводка имеет функцию анализа хода выполнения программы, которая может лучше определить, действительно ли код будет вызывать побочные эффекты.

Некоторые студенты могут подумать, что даже получение свойств объекта будет иметь побочные эффекты и приведет к тому, что код не будет удален, а это уже слишком! Это так, позвольте мне опубликовать еще один пример, чтобы продемонстрировать:портал

код показывает, как показано ниже:

// maths.js
export function square ( x ) {
	return x.a
}
square({ a: 123 })

export function cube ( x ) {
	return x * x * x;
}
//main.js
import { cube } from './maths.js';
console.log( cube( 5 ) ); // 125

Результат упаковки такой:

function square ( x ) {
  return x.a
}
square({ a: 123 });

function cube ( x ) {
	return x * x * x;
}
console.log( cube( 5 ) ); // 125

И еслиsquareв методеreturn x.aизменить наreturn x, окончательный упакованный результат не появитсяsquareметод. Конечно, если нетmaths.jsвыполнить это в файлеsquareметод, естественно, он не появится в файле пакета.

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

Глядя на конкретную конфигурацию uglify, мы можем знать, что в настоящее время uglify можно настроитьpure_getters: trueЧтобы заставить получение свойств объекта не иметь побочных эффектов. Это позволяет удалитьsquareметод. Однако, поскольку нетpure_settersТакая конфигурация,_createClassМетоды по-прежнему считаются побочными эффектами и не могут быть удалены.

так что нам делать?

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

Давайте сначала обсудим первопричины.Если мы хотим разработать библиотеку компонентов для использования другими, что нам делать?

Если вы используете webpack для упаковки библиотеки JavaScript

Сначала вставьте веб-пакет, чтобы упаковать проект как библиотеку JS.Документация. Как видите, в webpack есть множество режимов экспорта, в общем, каждый выберет наиболее универсальный.umdобразом, но вебпак не поддерживает режим экспорта модулей ES.

Поэтому, если вы упаковываете все файлы ресурсов в бандл-файл через webpack, то этот библиотечный файл никогда не будет связан с Tree-shaking.

тогда что нам делать? Это возможно. В настоящее время популярные в отрасли библиотеки компонентов в основном упаковывают каждый компонент или функцию в отдельный файл или каталог. Затем его можно импортировать следующим образом:

import clone from 'lodash/clone'

import Button from 'antd/lib/button';

Но это также более проблематично и не может одновременно вводить несколько компонентов. Таким образом, эти более популярные библиотеки компонентов, такие как antd и element, имеют специально разработанные подключаемые модули babel, так что пользователи могут использоватьimport { Button, Message } form 'antd'Этот способ загружен по требованию. По сути, преобразуется через плагин к предыдущему предложению, следующим образом:

import Button from 'antd/lib/button';
import Message from 'antd/lib/button';

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

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

{
  "name": "my-package",
  "main": "dist/my-package.umd.js",
  "module": "dist/my-package.esm.js"
}

Таким образом, когда разработчик загружает пакет npm в виде модуля es6, он будетmoduleЗначение — это файл записи, чтобы он мог быть совместим с несколькими методами импорта одновременно (поддерживаются как rollup, так и webpack2+). Но webpack не поддерживает экспорт в виде модулей es6, так что с webpack все же придется попрощаться. Мы должны попасть на сборы!

(Кому-то будет любопытно, то выставляйте входной документ ресурса перед пакетом передmodule, пусть пользователь скомпилирует и упакует его самостоятельно, тогда он сможет использовать нескомпилированную версию пакета npm для встряхивания дерева. Это действительно не невозможно. Однако конфигурация компиляции babel во многих инженерных проектах фактически игнорируется для повышения скорости компиляции.node_modulesфайлы внутри. Таким образом, чтобы обеспечить использование этих студентов, мы все равно должны предоставить скомпилированный модуль ES6. )

Упаковка библиотек JavaScript с накопительным пакетом

После стольких потерь мы, наконец, понимаем, что библиотека инструментов упаковки, библиотека компонентов или накопительный пакет просты в использовании, почему?

  1. Он поддерживает пакеты, которые экспортируют модули ES.
  2. Он поддерживает анализ потока программы и может более правильно определить, есть ли у кода самого проекта побочные эффекты.

Нам нужно только распечатать два файла через накопитель, одну версию umd и одну версию модуля ES, и их пути соответственно установлены вmain,moduleценность . Это облегчает пользователям выполнение встряхивания дерева.

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

Используйте webpack для упаковки инженерных проектов

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

Сначала нам нужно удалить Babel-Loader, а затем веб-пакет завершен, а затем выполнить файл Cabel Compilation. Однако, поскольку проекты WebPack часто имеют несколько входных файлов или разделение кода, нам нужно написать файл конфигурации, который соответствует Babel, который слегка хлопот. Таким образом, мы можем использовать плагин WebPack, так что эта часть все еще работает в процессе упаковки WebPack, какuglifyjs-webpack-pluginТак как это уже не один ресурсный файл в виде Loader, а скомпилированный в конечную ссылку. Здесь вам может понадобиться узнать о WebPackмеханизм плагина.

По поводу uglifyjs-webpack-plugin, вот небольшая деталь, webpack по умолчанию принесет более низкую версию, которую можно использовать напрямуюwebpack.optimize.UglifyJsPluginпсевдоним для использования. В частности, вы можете увидеть веб-пакетСвязанные инструкции

webpack =< v3.0.0 currently contains v0.4.6 of this plugin under webpack.optimize.UglifyJsPlugin as an alias. For usage of the latest version (v1.0.0), please follow the instructions below. Aliasing v1.0.0 as webpack.optimize.UglifyJsPlugin is scheduled for webpack v4.0.0

и этоНизкая версия uglifyjs-webpack-pluginиспользуемые зависимостиuglifyjsТакже в более низкой версии у него нетuglifyВозможность кода ES6, поэтому, если у нас есть такие требования, нам нужно перепроектироватьnpm install uglifyjs-webpack-plugin -D, установите последнюю версиюuglifyjs-webpack-plugin, снова введите его и используйте.

После этого мы используем плагин WebPack Babel для компиляции кода.

Проблема приходит снова, такой спрос относительно невелик, поэтому ни WebPack, ни Babel официально не имеет такого плагина, только сторонний разработчик разработал плагинbabel-webpack-plugin. К сожалению, автор не поддерживал этот плагин почти год, и есть проблема, этот плагин не будет использовать корневую директорию проекта..babelrcфайл для компиляции babel. кто-то упоминал об этомissue, но нет ответа.

Тогда никак, давайте я напишу новый плагин----webpack-babel-plugin, с его помощью мы можем позволить webpack скомпилировать код с помощью babel, прежде чем окончательно упаковать файл.Как установить и использовать, можно нажать на проект для просмотра. Обратите внимание, что эта конфигурация должна бытьuglifyjs-webpack-pluginПосле этого вот так:

plugins: [
  new UglifyJsPlugin(),
  new BabelPlugin()
]

Но на этом пути есть проблема, так как на финальном этапе babel компилирует относительно большие файлы, это занимает много времени, поэтому рекомендуется различать режим разработки и режим производства. Есть и большая проблема,webpackКомпилятор, используемый сам по себеacornОператор распространения (...) на объектах не поддерживается и некоторые функции, которые еще не являются официально стандартом ES, т.е. . . . .

Так что, если функция используется очень продвинуто, она все равно необходимаbabel-loader,ноbabel-loaderЧтобы выполнить специальную настройку, скомпилируйте код, все еще находящийся на этапе es, в код ES2017, чтобы облегчитьwebpackразобраться с собой.

Благодаря советам восторженных пользователей сети Nuggets, также есть плагинBabelMinifyWebpackPlugin, это зависит отbabel/minifyТакже интегрирует uglifyjs. Использование этого плагина эквивалентно эффекту использования UglifyJsPlugin + BabelPlugin выше.Если у вас есть это требование, рекомендуется использовать этот плагин.

Суммировать

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

  1. Старайтесь не писать код с побочными эффектами. Например, написание функции немедленного выполнения и использование внешних переменных в функции.
  2. Если требования к семантическим функциям ES6 не особенно строгие, вы можете включить babel.looseМод, об этом следует судить по собственному проекту, например: действительно ли нужно не перечислять атрибуты класса.
  3. Если вы разрабатываете библиотеку JavaScript, используйте rollup. И укажите версию модуля ES6, адрес файла записи установлен на package.jsonmoduleполе.
  4. Если при разработке библиотеки JavaScript неизбежно генерируются различные коды побочных эффектов, функциональные функции или компоненты могут быть упакованы в отдельные файлы или каталоги, чтобы пользователи могли загружать их через каталог. Если позволяют условия, вы также можете разработать отдельный webpack-загрузчик для собственной библиотеки, который пользователям удобно загружать по требованию.
  5. Если это разработка инженерного проекта, для зависимых компонентов можно только увидеть, есть ли у поставщика компонента оптимизации, соответствующие указанным выше пунктам 3 и 4. Для собственного кода, кроме пунктов 1 и 2, если у вас экстремальные требования к проекту, то можно его сначала запаковать, а потом уже скомпилировать окончательно.
  6. Если вы очень уверены в проекте, вы можете пройти некоторые uglifyКонфигурация сборки,Такие как:pure_getters: true, удалите некоторый код, который вынужден думать, что у него не будет побочных эффектов.

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

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

--читать оригинал

@Сиреневый сад F2E @Сян Сюэчан

--Пожалуйста, получите мое разрешение перед перепечаткой.