preface

The JS SDK builds Webpack VS Rollup and the code packaging volume of Webpack and Rollup is doubled. The main reason is that the Tree Shaking algorithm is different. Webpack Tree Shaking source code

Tree Shaking

Mechanism is briefly

Tree shaking was first proposed by the author of Rollup. Here’s a metaphor:

If you think of code packaging as making a cake. The traditional method is to throw in all the eggs (with the shell), stir them, put them in the oven, and then select and remove all the shells. Treeshaking, on the other hand, starts with a useful white egg yolk and whisks it into the cake.

Therefore, it is obviously more secure to find used code than unused code. Tree Shaking is finding used code, leaving unused code behind, and annotating it separately.

After separating the used and unused code, the unused code is removed through the compressor.

Use the premise

Since Tree Shaking uses ES6 Import and Export implementations to find used and unused code, Tree Shaking uses the following prerequisites: The source code must follow the ES6 module specification (import & export), if the CommonJS specification (require) cannot be used.

Webpack – Tree Shaking

The example analysis

Close the optimization

Webpack only turns Tree Shaking on in Production mode, so you need to set mode to Production. As you can see from the Tree Shaking mechanism in the previous section, we need to turn off Webpack’s code compressor to see how Webpack uses code, so we need to turn off Webpack optimization.

const path = require('path')

module.exports = {
    entry: './src/index.js'.output: {
        filename: 'bundle.js'.path: path.resolve(__dirname, 'dist')},mode: 'production'.optimization: {
        minimize: false.concatenateModules: false
    },
    devtool: false
}
Copy the code

util.js

export function usedFunction() {
    return 'usedFunction'
}

export function unusedFunction() {
    return 'unusedFunction'
}
Copy the code

index.js

import {
    usedFunction,
    unusedFunction
} from './util'

let result1 = usedFunction()
// let result2 = unusedFunction()

console.log(result1)
Copy the code

Package the main part of the result bundle.js (sure enough, see Webpack’s code usage notes)

/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ * * * * * * /
([
    /* 0 */
    / * * * /
    (function(module, __webpack_exports__, __webpack_require__) {

        "use strict";
        /* harmony export (binding) */
        __webpack_require__.d(__webpack_exports__, "a".function() {
            return usedFunction;
        });
        /* unused harmony export unusedFunction */
        function usedFunction() {
            return 'usedFunction'
        }

        function unusedFunction() {
            return 'unusedFunction'
        }

        / * * * /
    }),
    / * 1 * /
    / * * * /
    (function(module, __webpack_exports__, __webpack_require__) {

        "use strict";
        __webpack_require__.r(__webpack_exports__);
        /* harmony import */
        var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);

        let result1 = Object(_util__WEBPACK_IMPORTED_MODULE_0__[ /* usedFunction */ "a") ()// let result2 = unusedFunction()

        console.log(result1)

        / * * * /
    })
    / * * * * * * /
]);
Copy the code

Obviously: WebPack is responsible for tagging the code, marking import & Export as class 3

  • The used export is marked /* Harmony export ([type]) */, where [type] is related to webpack and can be binding, immutable, etc.
  • An unused export is marked as /* unused Harmony export [FuncName] */, where [FuncName] is the name of the export method
  • All imports are marked as /* harmony import */

Open the optimization

const path = require('path')

module.exports = {
    entry: './src/index.js'.output: {
        filename: 'bundle.js'.path: path.resolve(__dirname, 'dist')},mode: 'production'.optimization: {
        minimize: true.concatenateModules: true
    },
    devtool: false
}
Copy the code

Packing results:

! function(e) {
    var t = {};

    function n(r) {
        if (t[r]) return t[r].exports;
        var o = t[r] = {
            i: r,
            l:!1.exports: {}};return e[r].call(o.exports, o, o.exports, n), o.l = !0, o.exports
    }
    n.m = e, n.c = t, n.d = function(e, t, r) {
        n.o(e, t) || Object.defineProperty(e, t, {
            enumerable:!0.get: r
        })
    }, n.r = function(e) {
        "undefined"! =typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, {
            value: "Module"
        }), Object.defineProperty(e, "__esModule", {
            value:!0
        })
    }, n.t = function(e, t) {
        if (1 & t && (e = n(e)), 8 & t) return e;
        if (4 & t && "object"= =typeof e && e && e.__esModule) return e;
        var r = Object.create(null);
        if (n.r(r), Object.defineProperty(r, "default", {
                enumerable:!0.value: e
            }), 2 & t && "string"! =typeof e)
            for (var o in e) n.d(r, o, function(t) {
                return e[t]
            }.bind(null, o));
        return r
    }, n.n = function(e) {
        var t = e && e.__esModule ? function() {
            return e.default
        } : function() {
            return e
        };
        return n.d(t, "a", t), t
    }, n.o = function(e, t) {
        return Object.prototype.hasOwnProperty.call(e, t)
    }, n.p = "", n(n.s = 0)
}([function(e, t, n) {
    "use strict";
    n.r(t);
    console.log("usedFunction")}]);Copy the code

Obviously, code will be streamlined based on code annotations, and everything that doesn’t work will be removed.

Case Analysis summary

Webpack Tree Shaking is divided into two steps:

  • The first step is to annotate code usage
  • The second step is to delete all unused code.

Webpack Tree Shaking source code analysis

Static analysis of Webpack code, annotating code usage

By searching Webpack source code, the section containing Harmony export can find the specific implementation of used export and unused export annotation:

lib/dependencies/HarmonyExportInitFragment.js

class HarmonyExportInitFragment extends InitFragment {
    / * * *@param {string} exportsArgument the exports identifier
     * @param {Map<string, string>} exportMap mapping from used name to exposed variable name
     * @param {Set<string>} unusedExports list of unused export names
     */
    constructor(exportsArgument, exportMap = EMPTY_MAP, unusedExports = EMPTY_SET) {
        super(undefined, InitFragment.STAGE_HARMONY_EXPORTS, 1."harmony-exports");
        this.exportsArgument = exportsArgument;
        this.exportMap = exportMap;
        this.unusedExports = unusedExports;
    }

    merge(other) {
        let exportMap;
        if (this.exportMap.size === 0) {
            exportMap = other.exportMap;
        } else if (other.exportMap.size === 0) {
            exportMap = this.exportMap;
        } else {
            exportMap = new Map(other.exportMap);
            for (const [key, value] of this.exportMap) {
                if (!exportMap.has(key)) exportMap.set(key, value);
            }
        }
        let unusedExports;
        if (this.unusedExports.size === 0) {
            unusedExports = other.unusedExports;
        } else if (other.unusedExports.size === 0) {
            unusedExports = this.unusedExports;
        } else {
            unusedExports = new Set(other.unusedExports);
            for (const value of this.unusedExports) { unusedExports.add(value); }}return new HarmonyExportInitFragment(
            this.exportsArgument,
            exportMap,
            unusedExports
        );
    }

    / * * *@param {GenerateContext} generateContext context for generate
     * @returns {string|Source} the source code that will be included as initialization code
     */
    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 The ${this.unusedExports.values().next().value
				  } */\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}`; }}Copy the code

harmony export

GetContent handles exportMap and replaces the original export

		const definePart =
		    this.exportMap.size > 0 ?
		    `/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(The ${this.exportsArgument
				  }, {${definitions.join(",")}\n/* harmony export */ }); \n` :
		    "";
		return `${definePart}${unusedPart}` ;
		}
Copy the code

unused harmony exports

GetContent handles unusedExports and replaces the original export

		const unusedPart =
		    this.unusedExports.size > 1 ?
		    `/* unused harmony exports ${joinIterableWithComma(
						this.unusedExports
				  )} */\n` :
		    this.unusedExports.size > 0 ?
		    `/* unused harmony export The ${this.unusedExports.values().next().value
				  } */\n` :
		    "";
Copy the code

lib/dependencies/HarmonyExportSpecifierDependency.js

Statement informs and unused, and calls to replace HarmonyExportInitFragment off the export in the source code

HarmonyExportSpecifierDependency.Template = class HarmonyExportSpecifierDependencyTemplate extends NullDependency.Template {
	/ * * *@param {Dependency} dependency the dependency for which the template should be applied
	 * @param {ReplaceSource} source the current replace source which can be modified
	 * @param {DependencyTemplateContext} templateContext the context object
	 * @returns {void}* /
	apply(
		dependency,
		source,
		{ module, moduleGraph, initFragments, runtimeRequirements, runtime }
	) {
		const dep = / * *@type {HarmonyExportSpecifierDependency} * / (dependency);
		const used = moduleGraph
			.getExportsInfo(module)
			.getUsedName(dep.name, runtime);
		if(! used) {const set = new Set(a); set.add(dep.name ||"namespace");
			initFragments.push(
				new HarmonyExportInitFragment(module.exportsArgument, undefined, set)
			);
			return;
		}

		const map = new Map(a); map.set(used,`/* binding */ ${dep.id}`);
		initFragments.push(
			new HarmonyExportInitFragment(module.exportsArgument, map, undefined)); }};Copy the code

lib/dependencies/HarmonyExportSpecifierDependency.js

Pass moduleGraph to get all export name values

	/**
	 * Returns the exported names
	 * @param {ModuleGraph} moduleGraph module graph
	 * @returns {ExportsSpec | undefined} export names
	 */
	getExports(moduleGraph) {
		return {
			exports: [this.name],
			terminalBinding: true.dependencies: undefined
		};
	}
Copy the code

ModuleGraph (Build graph structure of ES6 module specification)

Lib/modulegraph.js (too much code, not to show)

class ModuleGraph {
	constructor() {
		/ * *@type {Map<Dependency, ModuleGraphDependency>} * /
		this._dependencyMap = new Map(a);/ * *@type {Map<Module, ModuleGraphModule>} * /
		this._moduleMap = new Map(a);/ * *@type {Map<Module, Set<ModuleGraphConnection>>} * /
		this._originMap = new Map(a);/ * *@type {Map<any, Object>} * /
		this._metaMap = new Map(a);// Caching
		this._cacheModuleGraphModuleKey1 = undefined;
		this._cacheModuleGraphModuleValue1 = undefined;
		this._cacheModuleGraphModuleKey2 = undefined;
		this._cacheModuleGraphModuleValue2 = undefined;
		this._cacheModuleGraphDependencyKey = undefined;
		this._cacheModuleGraphDependencyValue = undefined; }...Copy the code

Call function in the corresponding ModuleGraph for code static analysis at different stages of processing, build the ModuleGraph to prepare for export and import annotations, etc.

Take Compilation as an example.

Compilation

Lib/compiler.js (part of the code) pushes the analyzed modules into ModuleGraph at compile time.

	/ * * *@param {Chunk} chunk target chunk
	 * @param {RuntimeModule} module runtime module
	 * @returns {void}* /
	addRuntimeModule(chunk, module) {
		// Deprecated ModuleGraph association
		ModuleGraph.setModuleGraphForModule(module.this.moduleGraph);

		// add it to the list
		this.modules.add(module);
		this._modules.set(module.identifier(), module);

		// connect to the chunk graph
		this.chunkGraph.connectChunkAndModule(chunk, module);
		this.chunkGraph.connectChunkAndRuntimeModule(chunk, module);

		// attach runtime module
		module.attach(this, chunk);

		// Setup internals
		const exportsInfo = this.moduleGraph.getExportsInfo(module);
		exportsInfo.setHasProvideInfo();
		if (typeof chunk.runtime === "string") {
			exportsInfo.setUsedForSideEffectsOnly(chunk.runtime);
		} else if (chunk.runtime === undefined) {
			exportsInfo.setUsedForSideEffectsOnly(undefined);
		} else {
			for (const runtime ofchunk.runtime) { exportsInfo.setUsedForSideEffectsOnly(runtime); }}this.chunkGraph.addModuleRuntimeRequirements(
			module,
			chunk.runtime,
			new Set([RuntimeGlobals.requireScope])
		);

		// runtime modules don't need ids
		this.chunkGraph.setModuleId(module."");

		// Call hook
		this.hooks.runtimeModule.call(module, chunk);
	}
Copy the code

Source code analysis summary

  1. Webpack puts the discovered modules into the ModuleGraph at compile time
  2. HarmonyExportSpecifierDependency HarmonyImportSpecifierDependency and identify the import and export of the module.
  3. HarmonyExportSpecifierDependency recognition, informs the export and unused export
  4. 2 and unused

4.1 Replace export of used export with /* harmony export ([type])/import 4.2 Replace export of unused export with/unused harmony export [FuncName] */

Based on the above source code analysis, it can be marked as export and import units

Export class example for Tree Shaking

util.js

export default class Util {
usedFunction () {
  return 'usedFunction'
}

unusedFunction () {
  return 'unusedFunction'}}Copy the code

index.js

import Util from './util'

let util = new Util()

let result1 = util.usedFunction()
// let result2 = unusedFunction()

console.log(result1)

Copy the code

Package the main parts of the result bundle.js

"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a".function() { return Util; });
class Util {
usedFunction () {
  return 'usedFunction'
}

unusedFunction () {
  return 'unusedFunction'}}/ * * * / }),
/ * 1 * /
/ * * * / (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);


let util = new _util__WEBPACK_IMPORTED_MODULE_0__[/* default */ "a"] ()let result1 = util.usedFunction()
// let result2 = unusedFunction()

console.log(result1)


/ * * * / })
/ * * * * * * / ]);
Copy the code

Open optimization’s packaging results

!function(e){var n={};function t(r){if(n[r])return n[r].exports;var u=n[r]={i:r,l:!1.exports: {}};return e[r].call(u.exports,u,u.exports,t),u.l=!0,u.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0.get:r})},t.r=function(e){"undefined"! =typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule", {value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"= =typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default", {enumerable:!0.value:e}),2&n&&"string"! =typeof e)for(var u in e)t.d(r,u,function(n){return e[n]}.bind(null,u));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="",t(t.s=0)} ([function(e,n,t){"use strict"; t.r(n);let r=(new class{usedFunction(){return"usedFunction"}unusedFunction(){return"unusedFunction"}}).usedFunction();console.log(r)}]);
Copy the code

Function, the unused part of the class, is not marked unused export in the package result. The available Webpack is marked for the class as a whole (marked as being used), not individually for internal methods.

Combined with examples and source code analysis summary:

  • Write code using ES6 module syntax so Tree Shaking works.
  • The utility class functions should be output as separate functions, not as a single object or class, and avoid packaging objects and unused parts of the class.

The author



Benny Shi