likes
comments
collection
share

深入 Webpack 的 Tree Shaking

作者站长头像
站长
· 阅读数 87

前言

Tree Shaking

机制简述

tree shaking 是 rollup 作者首先提出的。这里有一个比喻:

如果把代码打包比作制作蛋糕。传统的方式是把鸡蛋(带壳)全部丢进去搅拌,然后放入烤箱,最后把(没有用的)蛋壳全部挑选并剔除出去。而 treeshaking 则是一开始就把有用的蛋白蛋黄放入搅拌,最后直接作出蛋糕。

因此,相对于找出 未使用 的代码,显然找出 已使用 的代码更有把握。 Tree Shaking 是先找出 已使用 的代码,自然剩下的则是 未使用 的代码,最后通过注释的方式分别标注。

区分 已使用 和 未使用 的代码后,通过 压缩器 将 未使用 的代码删除。

使用前提

由于 Tree Shaking 是通过 ES6 Import 和 Export 实现找出 已使用 和 未使用 的代码, 所以 Tree Shaking 使用前提: 是源码必须遵循 ES6 的模块规范 (import & export),如果是 CommonJS 规范 (require) 则无法使用。

Webpack - Tree Shaking

实例分析

关闭 optimization

Webpack 在 production 模式才会开启 Tree Shaking,所以需要把 mode 设置为 production。 由上一小节的 Tree Shaking 的 机制简述 可得,我们需要把 Webpack 的代码压缩器关闭才能看到 Webpack 对 代码使用情况的标注,所以需要关闭 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
}

util.js

export function usedFunction() {
    return 'usedFunction'
}

export function unusedFunction() {
    return 'unusedFunction'
}

index.js

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

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

console.log(result1)

打包结果 bundle.js 主要部分(果然看到了 Webpack 对代码使用情况的标注)

/************************************************************************/
/******/
([
    /* 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)

        /***/
    })
    /******/
]);

显然:webpack 负责对代码进行标记,把 import & export 标记为 3 类

  • 被使用过的 export 标记为 /* harmony export ([type]) */,其中 [type] 和 webpack 内部有关,可能是 binding, immutable 等等。
  • 没被使用过的 export 标记为 /* unused harmony export [FuncName] */,其中 [FuncName] 为 export 的方法名称
  • 所有 import 标记为 /* harmony import */

开启 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
}

打包结果:

! 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")
}]);

显然,会在 代码标注的基础上 进行代码精简,把没用的都删除。

实例分析总结

Webpack Tree Shaking 分为两步走:

  • 第一步标注代码使用情况
  • 第二步对未使用的代码都删除。

Webpack Tree Shaking 源码分析

Webpack 代码静态分析,标注代码使用情况

通过搜索 Webpack 源码,包含 harmony export 的部分,发现对 used export 和 unused export 的标注具体实现:

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 ${
						this.unusedExports.values().next().value
				  } */\n` :
            "";
        const definitions = [];
        for (const [key, value] of this.exportMap) {
            definitions.push(
                `\n/* harmony export */   ${JSON.stringify(
					key
				)}: ${runtimeTemplate.returningFunction(value)}`
            );
        }
        const definePart =
            this.exportMap.size > 0 ?
            `/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(${
						this.exportsArgument
				  }, {${definitions.join(",")}\n/* harmony export */ });\n` :
            "";
        return `${definePart}${unusedPart}` ;
    }
}

harmony export

getContent 处理 exportMap,对原来的 export 进行 replace

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

unused harmony exports

getContent 处理 unusedExports,对原来的 export 进行 replace

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

lib/dependencies/HarmonyExportSpecifierDependency.js

声明 used 和 unused,调用 HarmonyExportInitFragment 进行 replace 掉 源码里的 export

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();
			set.add(dep.name || "namespace");
			initFragments.push(
				new HarmonyExportInitFragment(module.exportsArgument, undefined, set)
			);
			return;
		}

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

lib/dependencies/HarmonyExportSpecifierDependency.js

传入 moduleGraph 获取所有 export 的 name 值

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

moduleGraph (建立 ES6 模块规范的图结构)

lib/ModuleGraph.js (该处代码量过多,不作展示)

class ModuleGraph {
	constructor() {
		/** @type {Map<Dependency, ModuleGraphDependency>} */
		this._dependencyMap = new Map();
		/** @type {Map<Module, ModuleGraphModule>} */
		this._moduleMap = new Map();
		/** @type {Map<Module, Set<ModuleGraphConnection>>} */
		this._originMap = new Map();
		/** @type {Map<any, Object>} */
		this._metaMap = new Map();

		// Caching
		this._cacheModuleGraphModuleKey1 = undefined;
		this._cacheModuleGraphModuleValue1 = undefined;
		this._cacheModuleGraphModuleKey2 = undefined;
		this._cacheModuleGraphModuleValue2 = undefined;
		this._cacheModuleGraphDependencyKey = undefined;
		this._cacheModuleGraphDependencyValue = undefined;
	}
......

在不同的处理阶段中调用对应的 ModuleGraph 里的function 做代码静态分析,构建 moduleGraph 为 export 和 import 标注等等操作做准备。

以 Compilation 为例。

Compilation

lib/Compilation.js (部分代码) 在 编译阶段 中将分析所得 的 module 入栈到 ModuleGraph。

	/**
	 * @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 of chunk.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);
	}

源码分析总结

  1. Webpack 在编译阶段将发现的 modules 放入 ModuleGraph
  2. HarmonyExportSpecifierDependency 和 HarmonyImportSpecifierDependency 识别 import 和 export 的 module。
  3. HarmonyExportSpecifierDependency 识别 used export 和 unused export
  4. used 和 unused 4.1 把 used export 的 export 替换为 /* harmony export ([type]) / import 4.2 把 unused export 的 export 替换为 / unused harmony export [FuncName] */

根据上述源码分析,可得以 export 和 import 为单位进行标记

在 Tree Shaking 的 export class 实例分析

util.js

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

unusedFunction () {
  return 'unusedFunction'
}
}

index.js

import Util from './util'

let util = new Util()

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

console.log(result1)

打包结果 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)


/***/ })
/******/ ]);

开启 optimization 的打包结果

!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)}]);

由打包结果中,class未使用的部分function并没有标注 unused export。 可得 webpack 是对类整体进行标记的(标记为被使用),而不是分别针对内部方法。

结合 实例 和 源码分析 总结:

  • 使用 ES6 模块语法编写代码, 这样 Tree Shaking 才能生效。
  • 工具类函数尽量以单独的形式输出,不要集中成一个对象或者类,避免打包对象和类未使用的部分。

作者

深入 Webpack 的 Tree Shaking Benny Shi
转载自:https://juejin.cn/post/6866747701908733966
评论
请登录