Build the column series directory entry

Zuo Lin, Front end development engineer of Wedoctor Front End Technology Department. In the Internet wave, love life and technology.

preface

If you read this article in the Rollup series – where does the useless code go? You’re no stranger to tree-shaking projects rollup. If you are not familiar with tree-shaking, take 5 minutes to read this article: What is tree-shaking?

As we all know, Webpack, which did not support tree-shaking, implemented tree-shaking in its 2.x version. Curious again, Rollup implemented tree-shaking from the start. Webpack is implemented in 2.x after seeing rollup’s packaging slimming effect. Is it the same for tree-shaking?

Because of such questions, there is this article.

Tree-shaking implementation mechanism

After a quick scan of official documentation and articles, there is more than one way webPack can implement tree-shaking! However, both are different from rollup.

Early Webpack configuration is not easy to use, so there was a WebPack configuration engineer jokingly, although now webPack configuration is greatly simplified, Webpack4 also declared 0 configuration, but if it involves complex comprehensive packaging function, is not 0 configuration can be achieved. It is useful to know how it works and how it is configured, so let’s take a look at how WebPack implements tree-shaking.

Tree-shaking — rollup VS Webpack

  • Rollup analyzes the flow during compilation and packaging, and thanks to ES6 static modules (exports and imports cannot be modified at run time), we can determine which code we need at package time.

  • Webpack itself can only mark unused code and not remove it when it is packaged, and it is tools like UglifyJS, Babili, Terser, etc., that identify unused code and do tree-shaking. In simple terms, the compression tool reads the webPack results and removes unused code from the bundle prior to compression.

We talked about tagging unused code, and we talked about compression tools like UglifyJS, Babili, Terser, etc. How does WebPack and compression tools bring tree-shaking to life? Let’s take a look at the history of implementing tree-shaking in Webpack.

Webpack implements three phases of tree-shaking

Phase 1: UglifyJS

The earliest version of webpack implementation tree-shaking: How to use it in WebPack 2 If you don’t want to take the time to dig, take a look at this:

  • UglifyJS does not support ES6 or higher. You need to use Babel to compile the code to ES5, and then use UglifyJS to remove useless code.
  • Compile code to ES5 using Babel, but preset ES6 modules not to be swayed by the Babel preset, and preset plugins for Webpack accordingly;
  • To avoid side effects, mark it as pure so UglifyJS can handle it, mainly because the compilation of Webpack prevents tree-shaking of classes, it only works for functions, This was later resolved by supporting the assignment of the compiled class to be marked @__pure__.
// .babelrc
{
  "presets": [["env", {
      "loose": true.// Loose mode
      "modules": false // Do not convert modules, keep ES6 syntax}}]]Copy the code
// webpack.config.js
module: {
  rules: [{test: /\.js$/, loader: 'babel-loader'}},plugins: [
  new webpack.LoaderOptionsPlugin({
    minimize: true.debug: false
  }),
  new webpack.optimize.UglifyJsPlugin({
    compress: {
      warnings: true
    },
    output: {
      comments: false
    },
    sourceMap: false})]Copy the code

Stage 2: BabelMinify

Babili, later renamed BabelMinify, is a code compression tool based on Babel. Babel has already understood the new syntax through our parser Babylon, and has integrated the compression function of UglifyJS into Babili. In essence, it achieves the same function as UglifyJS. However, using babili plug-in, it does not need to translate, but directly compresses. Make your code smaller.

There are two ways to use Babili to replace Uglify: Babili plug-in and babel-loader default. Babel Minify is best suited for the latest browsers (with full ES6+ support) and can also be used with the usual Babel ES2015 presets to compile code down first, as explained at the end of the official documentation.

Using Babel-Loader in webpack and then introducing Minify as a preset will run faster than using the BabelMinifyWebpackPlugin directly (covered next). Because babel-Minify processes smaller files.

Stage 3: Terser

Terser is a JavaScript parser and Mangler/Compressor toolkit for ES6+. If you’ve read the issue, you’ll know that more and more people are abandoning Uglify for Terser, and it’s clear why:

  • Uglify is no longer maintained and does not support ES6+ syntax
  • Webpack comes built-in with the Terser plug-in for code compression by default

For sideEffects, unused module detection has been extended from webpack 4 to provide compiler with hints via the package.json “sideEffects” property as a marker. Indicate which files in the project are “pure “so that unused portions of the file can be safely removed.

In webpack4, you had to configure the compression plugin manually, but the latest webpack5 has tree-shaking built in. Tree-shaking! In a production environment requires no configuration

Tree-shaking process for Webpack

Webpack markup code

In general, Webpack marks the code, mainly for import & export statements, as three classes:

  • All imports are marked as/* harmony import */
  • All used exports are marked as/* harmony export ([type]) */, including[type]Binding, immutable, and so on
  • The unused export is marked as/* unused harmony export [FuncName] */, including[FuncName]Is the method name of export

First of all, in order to run a business project properly, Webpack needs to bundle the business code written by the developer and the runtime that supports and deploys that business code. Down to the Webpack source implementation, the runtime generation logic can be divided into two steps in the packaging phase:

  • Dependency collection: Iterate through code modules and collect feature dependencies for modules to determine a list of Webpack Runtime dependencies for the entire project;
  • Build: Merge the runtime dependency list and package it into the final output bundle.

Obviously, statement marking of code occurs during dependency collection.

Mark all imports in the runtime environment:

const exportsType = module.getExportsType(
	chunkGraph.moduleGraph,
	originModule.buildMeta.strictHarmonyModule
);
runtimeRequirements.add(RuntimeGlobals.require);
const importContent = `/* harmony import */ ${optDeclaration}${importVar} = __webpack_require__(${moduleId}); \n`;

// Dynamically import parsing
if (exportsType === "dynamic") {
	runtimeRequirements.add(RuntimeGlobals.compatGetDefaultExport);
	return [
		importContent, // tag /* harmony import */
		`/* harmony import */ ${optDeclaration}${importVar}_default = /*#__PURE__*/${RuntimeGlobals.compatGetDefaultExport}(${importVar}); \n` // The /*#__PURE__*/ comment tells Webpack that a function call has no side effects
	]; // Return import statements and compat statements
}
Copy the code

Mark all used and unused exports in the runtime environment:

	// State property getters at runtime
  generate() {
		const { runtimeTemplate } = this.compilation;
		const fn = RuntimeGlobals.definePropertyGetters;
		return Template.asString([
			"// define getter functions for harmony exports".`${fn} = ${runtimeTemplate.basicFunction("exports, definition"[`for(var key in definition) {`,
				Template.indent([
					`if(${RuntimeGlobals.hasOwnProperty}(definition, key) && !${RuntimeGlobals.hasOwnProperty}(exports, key)) {`,
					Template.indent([
						"Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });"
					]),
					"}"
				]),
				"}"
			])}; `
		]);
	}
  
  // Enter generate context
  getContent({ runtimeTemplate, runtimeRequirements }) {
		runtimeRequirements.add(RuntimeGlobals.exports);
		runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);

		const unusedPart =
			this.unusedExports.size > 1
				? `/* unused harmony exports ${joinIterableWithComma(
						this.unusedExports
				  )} */\n`
				: this.unusedExports.size > 0
				? `/* unused harmony export ${first(this.unusedExports)} */\n`
				: "";
		const definitions = [];
		for (const [key, value] of this.exportMap) {
			definitions.push(
				`\n/* harmony export */   The ${JSON.stringify(
					key
				)}: ${runtimeTemplate.returningFunction(value)}`
			);
		}
		const definePart =
			this.exportMap.size > 0
				? `/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(The ${this.exportsArgument
				  }, {${definitions.join(",")}\n/* harmony export */ }); \n`
				: "";
		return `${definePart}${unusedPart}`; // The source code included as initialization code}}Copy the code

Compression clearance method

UglifyJS

Take UglifyJS as an example. UglifyJS is a parser, minifier, compressor, and beautifier toolkit. For details, please refer to UglifyJS Chinese manual.

If you don’t want to browse through such a long document, take a look at the clean, tree-shaking summary of compression configuration parameters.

  • dead_codeRemove unreferenced code // Does that look familiar? Useless code!
  • drop_debugger– remove the debugger
  • unused— Kills functions and variables that are not referenced. (A simple direct assignment to a variable is not referenced unless “keep_assign” is set.)
  • toplevel— Kill functions (“funcs”) and/or variables (“vars”) that are not referenced in the top-level scope (default: false, true: all function variables are eliminated)
  • warnings– displays a warning when deleting useless code
  • pure_getters— The default is false. If you pass true, UglifyJS assumes that references to object properties (such as foo.bar or foo[“bar”]) have no function side effects.
  • pure_funcsYou can pass in an array of names, and UglifyJS will assume that these functions have no side effects.

Here’s an example:

plugins: [
  new UglifyJSPlugin({
    uglifyOptions: {
      compress: {
          // Then the function is considered to have no function side effects and the whole declaration is discarded. In the current execution case, there is an overhead (compression is slow).
          pure_funcs: ['Math.floor']}}})],Copy the code

If the name is redefined in scope, it will not be checked again. For example, var q = math.floor (a/b), if the variable q is not referenced, UglifyJS will kill it, but math.floor (a/b) will be kept, and no one knows what it does.

  • Side_effects — Default true. Pass false to disable discarding pure functions. If a function is called with a slash before it is called@PURE/ or /#PURE/ comment, the function will be marked as pure. For example, /@PURE/foo();

In fact, with so many compression configurations, in addition to manual configuration to address side effects, the UglifyJS default configuration is used to remove useless markup code for tree-shaking.

terser

Take Terser, a JavaScript parser and Mangler/Compressor toolkit for ES6+. Please check the official document for details. There is no documentation in Chinese, but a quick glance shows that the configuration parameters are not that different from UglifyJS. Of course, there are obviously some additional parameters:

  • arrowsClass and object literal methods are also converted to arrow expressions if the converted code is shorter
  • ecma— Enable compression options with ES2015 or later to convert ES5 code to a smaller ES6+ equivalent form

This is obviously because Terser supports ES6+ syntax, which is one of its advantages in making UglifyJS obsolete.

Compression performance PK

Currently, Webpack has been updated to version 5.x, and the Terser plugin is built-in by default and does not need to be configured. Although the TerserPlugin is used by default in production environments and is a good choice for code compression, there are other options available. Wait, isn’t our topic tree-shaking? How the compression tool suddenly went further and further…

In essence, it is the compression tools that implement tree-shaking, so the performance of the compression tools seems to be fine!

Compression works in production, so it’s tree-shaking in production. The following three configurable plug-ins require a webPack version of at least V4+.

UglifyjsWebpackPlugin

The basic usage is also simpler:

// webpack.config.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [new UglifyJsPlugin()],
  },
};

const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
  plugins: [
    new UglifyJsPlugin()
  ]
}
Copy the code

BabelMinifyWebpackPlugin

There are two ways to replace UglifyJS with babili plug-in and babel-loader default.

Babili plug-in

Instead of uglify, use Babili instead of Uglify. There is no need for babel-loader:

// webpack.config.js
const MinifyPlugin = require("babel-minify-webpack-plugin");
module.exports = {
  plugins: [
    new MinifyPlugin(minifyOpts, pluginOpts)
  ]
}
Copy the code

Babel – loader preset

Babel Minify is best suited for the latest browsers (with full ES6+ support) and can also be used with the usual Babel ES2015 presets to compile code down first, as explained at the end of the official documentation.

Using Babel-Loader in webpack and then introducing Minify as a preset plugin will run faster than using BabelMinifyWebpackPlugin directly. Because babel-Minify processes smaller files.

Babelrc is configured as follows:

{
  "presets": ["es2015"]."env": {
    "production": {
      "presets": ["minify"]}}}Copy the code

But the BabelMinifyWebpackPlugin must exist for its irreplaceable purpose:

  • Webpack Loader operates on a single file, and Minify Preset, as a Webpack Loader, treats each file as being executed directly in browser global scope (default) and doesn’t optimize something in top-level scope;
  • When excluding node_modules is not run through babel-loader, the babel-minify optimization does not apply to excluded files;
  • When using babel-Loader, the code generated by Webpack for the module system is not optimized by Babel-Minify;
  • The Webpack plug-in can run on the entire chunk/bundle output and can optimize the entire bundle.

Adopt the first method:

TerserWebpackPlugin

Like the Uglify and babelMinify plug-ins, the Terser plug-in is simple to configure and use.

webpack.config.js
const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true.minimizer: [new TerserPlugin()],
  },
};
Copy the code

It seemed to work as expected, and since my file code was small, the size advantage was not obvious, but the compression time was.

Official data performance comparison

Here’s a comparison from the Kangkang bableMinify document:

Pack the react:Packaging vue:

Packaging lodash:Packaging three. Js:

summary

Let’s take a lookIssue areaWhat the Internet had to say:

Terser compression performance is three times better than Uglify! Nice!

Given that the Terser-webpack-plugin is maintained and has more correctness fixes, it is definitely the first choice — worth switching even without the performance improvements (which it has). Final word: WebPack + Terser compression is the ultimate choice! Webpack5’s built-in terser says it all!

Treatment Side Effects

A “side effect” is defined as code that performs special behavior when importing, rather than just exposing one or more exports. For example, polyfill, which affects the global scope, usually does not provide export.

Side effects are also covered in rollup. Some module imports, once introduced, can have a significant impact on an application. Examples are global stylesheets, or JavaScript files that set global configuration.

Webpack argues that such files have “side effects” and that side-effect files should not be tree-shaking, as it will break the entire application. Webpack’s tree-shaking is not as good at dealing with side effects. It can easily determine whether a variable has been referenced or modified later, but it cannot determine the complete modification process of a variable. It does not know whether it has pointed to external variables, so many code that may cause side effects can only be conservative and not deleted.

Fortunately, we can configure the project to tell Webpack what code is side-effect-free and tree-shaking is possible.

Configuration parameters

In your project’s package.json file, add the “sideEffects” property. Package. json has a special property, sideEffects, which is designed to handle sideEffects — providing hints to the webpack compiler about which code is “pure.” It has three possible values:

  • True is the default if no other value is specified. This means that all files have the side effect of not having a file to tree-shaking.
  • False tells Webpack that no files have side effects and that all files are tree-shaking.
  • The third value […] Is an array of file paths. It tells WebPack that there are no side effects for any of your files other than those contained in the array. Therefore, except for the specified files, all files can be tree-shaking safely.
{
  "name": "your-project"."sideEffects": false
  // "sideEffects": [// Array mode supports relative path, absolute path and glob mode for related files
  // "./src/some-side-effectful-file.js",
  // "*.css"
  / /]
}
Copy the code

Each project must have the sideEffects property set to false or an array of file paths. If your code does have some sideEffects, you can provide an array instead, and the sideEffects tag needs to be configured properly to work.

Tag in code

The /#PURE/ annotation tells Webpack that a function call has no side effects. It is used to mark functions as free of side effects (pure) before they are called. An input parameter passed to a function cannot be marked by the comment above; each tag must be marked separately. If the initial value of an unused variable definition is considered pure, it is marked as dead code, will not be executed and will be cleared by the compression tool. The behavior is turned on when optimization.innerGraph is set to true, whereas in Webpack 5.x optimization.innerGraph defaults to true.

Level of grammar use

  • First, more optimizations are enabled when mode is Production, including the compressed code and tree shaking discussed in this article.
  • Use ES2015 module syntax (that is, import and export);
  • Make sure no compiler converts ES2015 module syntax to CommonJS, set modules in presets to false, and tell Babel not to compile module code.

conclusion

  • If you are developing a JavaScript library, use rollup! The ES6 Module version is provided, and the entry file address is set to the module field of package.json.
  • Using even older versions of WebPack can give preference to the Terser plugin as a compression tool;
  • To avoid side effects, try not to write code with side effects, use ES2015 module syntax;
  • In the project package.json file, add a sideEffects entry and set the sideEffects property to false, also via /#PURE/ comments force the removal of code that is thought to have no side effects;
  • An additional compression tool (eg. Terser) that can remove unreferenced code is also included in Webpack.

The resources

  • How to use tree-shaking in Webpack 2
  • Your tree-shaking is not good for eggs
  • UglifyJS Chinese manual
  • Webpack 4 Tree Shaking Ultimate Optimization Guide
  • Tree-shaking Webpack