This article will take a look at the current state of tree-shaking (webpack@3, babel@6 below), why tree-shaking is still struggling, and finally summarize some of the ways that tree-shaking can be improved today.

Tree-shaking is a term that many front-end programmers are familiar with. It basically means removing code that is not used. Such functionality is great for building large applications, where daily development often references libraries. Most of the time, however, only parts of these libraries are used, not all of them, and tree-shaking can greatly reduce the amount of packaged code if it helps remove unused code.

Tree-shaking was first proposed and implemented by Rollup in the front end, and the subsequent Webpack was also implemented by UglifyJS in version 2.x. Since then, tree-shaking has been seen in various articles discussing optimizing packaging.

Many developers are happy to see that libraries like elementUI and ANTD that they reference can finally be deleted. However, the ideal is full, the reality is backbone. After the upgrade, there are no significant changes to the project package.

I also encountered such a problem, some time ago, need to develop a component library. I wonder why, after packaging my component library, referees through ES6 still end up importing unused components from the component library.

I would like to share with you my experience of tree-shaking.

The principle of Tree – Shaking

Here I will post an article from Baidu Takeout front: Tree-shaking Performance Optimization Practices – Principles.

If you’re too lazy to read the article, here’s the summary:

  1. ES6’s module introductions are statically analyzed, so you can determine exactly what code is loaded at compile time.
  2. Analyze the program flow to determine which variables are not used or referenced, and then delete the code.

Good, the principle is perfect, so why can’t we delete our code?

First say the reason: are side effects of the pot!

Side effects

Those of you who are familiar with functional programming will be familiar with the term side effects. It can be roughly defined as the behavior of a function that affects, or may affect, variables outside the function.

For example, take this function:

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

This function modifies the global location variable and even causes the browser to jump, which is a side effect.

Now we know the side effects, but when you think about it, there are no side effects from the component library I wrote. Each component I wrote is a class, which can be simplified as follows:

// 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
  }
}
Copy the code
// main.js
import { Apple } from './components'

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

console.log(appleModel)
Copy the code

I tried tree-shaking with the rollup online repl, and did remove the Person, portal

But why can’t I eliminate unused code when I package a component library with Webpack and get brought in by others?

Because I missed two things: Babel compilation + Webpack packaging

You win Babel, you lose Babel

Babel takes ES6/ES7 code and converts it into code that specific browsers can support. Because of this, we front-end developers have the beautiful development environment we have today, with the ability to use the latest JavaScript language features without regard to browser compatibility.

However, it is also because of its compilation that some of the code we might have thought had no side effects was converted to have (possibly) side effects.

As in my example above, if we compile with Babel and paste it to the ROLLup repL, the result is: a portal

If you can’t be bothered to click on the link, see what happens when the Person class is compiled by Babel:

function _classCallCheck(instance, Constructor) { if(! (instanceinstanceof 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) {
    returnprotoProps && 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; }}]);returnPerson; } ();Copy the code

Our Person class is wrapped as an IIFE(execute function immediately) and returns a constructor. So how does it have side effects? The problem is with the _createClass method. You simply delete the _createClass call from the IIFE of the Person in the last rollup repL link, and the Person class is removed. As for why _createClass has side effects, let’s leave that aside. Another question might arise: why does Babel declare constructors this way?

If I were you, I would probably compile it like this:

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

  }
  Person.prototype.getName = function () { return this.name };
  returnPerson; } ();Copy the code

Because that’s how we used to write “classes”, so why does Babel take the form Object.defineProperty, and what’s wrong with a prototype chain? Naturally, this is inappropriate because some of the ES6 syntax is semantically specific. Such as:

  1. Methods declared inside a class are not enumerable, whereas methods declared through stereotype chains are enumerable. Here you can refer to ruan’s introduction of the basic grammar of Class
  2. for... ofThe loop is passed through the iterator (Iterator) iterates over the array instead of i++, and then searches for the value by subscript. Here you can still see ruan teacher about traverser and for… Of, and an article by Babel aboutfor... ofCompilation instructionstransform-es2015-for-of

So, in order to conform to the true semantics of ES6, Babel compiles classes with Object.defineProperty to define the prototype methods, which leads to all of these subsequent problems.

If you are sharp-eyed, you may have noticed from my transform-es2015-for-of link that Babel actually has a loose mode, which is literally called loose mode. What is it used for? Instead of following the semantics of ES6 strictly, it compiles code in a way that is more consistent with how we normally write code. For example, the property methods of the Person class above will be compiled to declare methods directly on the prototype chain.

The specific Babel configuration for this mode is as follows:

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

Also, I’ll put up an online repL example for you to see directly: loose-mode

Well, if we really didn’t care if class methods were enumerated, wouldn’t it be nice to tree-shaking classes with no side effects if we turned loose mode on?

We opened loose mode, used rollup packaging, and found it was true! portal

Bad UglifyJS

Don’t get too excited, though. When we use Webpack and UglifyJS to package files, the IIFE of the Person class is packaged again. What???

To get to the bottom of this issue, I found an issue of UglifyJS: Class Declaration in IIFE Considered as Side Effect and perused it for a long time. If you are interested in this issue and your English is ok, you can quickly understand this issue, which is quite interesting. Let me outline what is said under this issue.

Blacksonic wonders why UglifyJS can’t eliminate unreferenced classes.

UglifyJS contributor – KZC says that Uglify does not perform program flow analysis, so code with potential side effects cannot be ruled out.

My code has no side effects. Why don’t you come up with a configuration item, set it up, assume it has no side effects, and then delete it.

Contributor: We don’t have flow analysis, we can’t do it, we really want to delete them, go out and turn left rollup, they shit, do flow analysis to determine if there are any side effects.

The rollup cost is a bit high. I don’t think it’s hard to add a configuration, like this, blah, blah, blah.

Contributor: Welcome to PR.

Come on, your project has thousands of lines of code, I don’t mention PR. My code does not have any side effects, can you explain it in detail?

Contributor: Variable assignments can have side effects! Let me give you an example:

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())
Copy the code

Contributor: V6Engine is not used, but it modifies properties on the V8Engine prototype chain, which has side effects. The V6Engine rollup is a rollup strategy for the V6Engine.

Building Lord and a few passers-by yibingding, have put forward their own proposals and programs. Finally, it is possible to declare this function side-efficacy-free in code with comments such as /* @__pure__ */.

The uglify contributor KZC raised the issue of rollup at that time. Rollup thought the issue was not serious or urgent, and the contributor also offered a PR to Rollup to solve the problem.

Let me summarize the following key information from this issue:

  1. Function arguments that reference types can have adverse effects on operations on their attributes. Because it is a reference type in the first place, any changes to its properties actually change the data outside the function. Gets or modifies its propertiesgetterorsetterAnd thegetter,setterIt’s opaque and can cause side effects.
  2. Uglify has no perfect program flow analysis. It can simply judge whether a variable is referenced or modified later, but it cannot judge the complete modification process of a variable. We do not know whether it has pointed to external variables, so many codes that may have side effects can only be conservative and not deleted.
  3. Rollup has flow analysis to better determine if the code is actually causing side effects.

Some of you might think that even getting an object’s properties has the side effect of not being able to delete the code, which is too much. In fact, let me post another example to demonstrate this: portals

The code is as follows:

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

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

Copy the code

Packing results are as follows:

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

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

If return x.a is changed to return x in the square method, the final result will not have the square method. Of course, if you don’t implement the square method in your maths.js file, you won’t see it in your package file either.

So we now understand why the _createClass method compiled by Babel had side effects. Now, looking back, it was full of side effects.

Looking at uglify’s configuration, we can see that uglify can currently set pure_getters: true to force fetching object attributes without side effects. This allows you to remove the square method from the above example. However, since there is no such configuration as pure_setters, the _createClass method is still considered to have side effects and cannot be deleted.

So what do we do?

If you’re smart enough, you might think that Babel compilation is causing side effects, so let’s do tree-shaking packaging first, and compile the bundle at the end. This is indeed a solution, but unfortunately it works when dealing with the project’s own source code, not with externally dependent NPM packages. In order to make the tool kit universal, compatibility, mostly compiled through Babel. It is these external dependencies that account for the most capacity.

Let’s start at the root. What if we were to develop a component library for others to use?

If you use Webpack to package the JavaScript library

First post webpack to package the project as a JS library document. It can be seen that WebPack has a variety of export modes, generally people will choose the most universal UMD mode, but WebPack does not support the export ES module mode.

So, if you pack all your resource files into a bundle via Webpack, the library file is not tree-shaking anymore.

So what to do? It’s not impossible. The most popular component libraries in the industry pack each component or function into a separate file or directory. It can then be introduced as follows:

import clone from 'lodash/clone'

import Button from 'antd/lib/button';
Copy the code

But this can be cumbersome, and you can’t introduce multiple components at the same time. So some of the more popular component libraries like ANTD, Element, have developed a Babel plug-in that allows users to load the import {Button, Message} form ‘antd’ on demand. Essentially, the plugin converts the code from the previous sentence to the following:

import Button from 'antd/lib/button';
import Message from 'antd/lib/button';
Copy the code

This seems like the perfect variant tree-shaking solution. The only downside is that for component library developers, you need to develop a special Babel plug-in; For consumers, the need to introduce a Babel plug-in adds slightly to the cost of development and usage.

In addition, there is actually a more cutting-edge approach. Json, add a key: module to package.json, as follows:

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

This allows developers to load NPM packages as ES6 modules with module values as entry files, allowing for multiple import modes (rollup and WebPack2 + are both supported). But WebPack doesn’t support exporting as an ES6 module, so webPack is done. We need to rollup!

One might wonder, why not expose the unpackaged resource entry file to the Module and let the user compile and package it so that it can tree-shaking the uncompiled NPM package. It’s not impossible. However, the Babel compilation configuration of many engineering projects actually ignores the files in node_modules in order to speed up the compilation. So we should still expose a compiled copy of the ES6 Module for these students to use.

Package JavaScript libraries using rollup

Thanks to the package library, component library, or rollup library, we finally understand why it works.

  1. It supports exporting packages for the ES module.
  2. It supports program flow analysis and can more accurately determine whether the project’s own code has side effects.

We simply rollup two files, umD and ES, with paths set to main and Module respectively. This makes it easier for users to tree-shaking.

The problem is that users do not use rollup to package their engineering projects. Due to the lack of ecology and the limitations of code splitting and other functions, they usually use WebPack to package their engineering projects.

Package engineering projects using WebPack

As mentioned earlier, we can increase the effectiveness of tree-shaking by doing tree-shaking first and compiling later to reduce the side effects of compiling. So how do you do that?

First we need to remove the babel-loader, and then execute the Babel file after the webpack is finished. However, since WebPack projects often require multiple entry files or code splitting, we need to write a configuration file to implement Babel, which is a bit more cumbersome. Therefore, we can use webpack plugin to make this link still run in the packaging process of Webpack, just like uglifyjs-webpack-plugin. It is no longer operated on a single resource file in the form of loader, but compiled in the last link of packaging. You may need to take a look at the Plugin mechanism of WebPack.

About uglifyjs – webpack – the plugin, there is a small detail, webpack default will get a low version, you can directly use webpack. Optimize. UglifyJsPlugin alias to use. See the webpack documentation for details

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

The lower version of uglifyJs-webpack-plugin relies on uglifyJS as well. It does not have the capability of uglifyES6 code, so if we have such a requirement, NPM install uglifyjs-webpack-plugin -d uglifyjs-webpack-plugin -d uglifyjs-webpack-plugin -d uglifyjs-webpack-plugin -d uglifyjs-webpack-plugin -d uglifyjs-webpack-plugin -d

After that, we can compile the code using webPack’s Babel plug-in.

The problem is that there is less demand for such a plugin, so neither WebPack nor Babel officially have such a plugin, only a third party developer developed the plugin babel-webpack-plugin. Unfortunately, this author hasn’t maintained the plug-in in nearly a year, and there is a problem that the plug-in doesn’t compile Babel with the.babelrc file in the project root directory. Someone made an issue of it, but there was no response.

So there is no way, let me write a new plug-in —-webpack-babel-plugin, with it we can let Webpack before the final package Babel compiled code, specific how to install and use can click on the project to see. Note that this configuration needs to come after uglifyjs-webpack-plugin, like this:

plugins: [
  new UglifyJsPlugin(),
  new BabelPlugin()
]
Copy the code

However, one problem is that Babel takes a long time to compile large files in the last stage, so it is recommended to distinguish between development mode and production mode. An even bigger problem is that webPack’s own compiler, Acorn, does not support object extension operators. And some features that are not officially an ES standard yet, so…

So if the feature is very advanced, you still need babel-loader, but babel-Loader is specially configured to compile es Stage code into ES2017 code for webPack itself to handle.

Thanks to nuggets fans for their tips and a pluginBabelMinifyWebpackPluginIt depends onbabel/minifyUglifyjs is also integrated. Using this plug-in is equivalent to using UglifyJsPlugin + BabelPlugin. If there is a need in this respect, it is recommended to use this plug-in.

conclusion

With all that said, let me conclude by summarizing what we can do for tree-shaking at this stage.

  1. Try not to write code with side effects. Such as writing a function that executes immediately and using external variables in the function.
  2. If you are not particularly strict about ES6 semantic features, you can turn Babel onlooseMode, which depends on your project, such as whether you really want an unenumerable class property.
  3. If you are developing a JavaScript library, use rollup. The ES6 Module version is also provided, and the entry file address is set to package.jsonmoduleField.
  4. If JavaScript library development inevitably produces side effects, you can package functions or components into separate files or directories that users can easily load from. If possible, you can also develop a separate Webpack-loader for your own library, so that users can load it on demand.
  5. In the case of engineering project development, it is only up to the component provider to see if they have optimizations corresponding to points 3 and 4 above for dependent components. For their own code, except 1, 2 points, for the ultimate requirements of the project, can be packaged first, and then compile.
  6. If you’re really sure about the project, you can use some of Uglify’sCompile the configuration, such as:pure_getters: trueDelete code that is forced to assume no side effects.

Therefore, at this stage, there is still no simple and easy way for us to complete tree-shaking. So it’s really hard to do something well. It requires not only individual efforts, but also the course of history.

PS: THE code mentioned in this article has also been uploaded to Github. You can download it by clicking on the original article.

— Reading the passage

@Lilac Garden F2E @Phase senior

— Reprint please first through my authorization.