深入 Webpack 的 Tree Shaking
前言
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);
}
源码分析总结
- Webpack 在编译阶段将发现的 modules 放入 ModuleGraph
- HarmonyExportSpecifierDependency 和 HarmonyImportSpecifierDependency 识别 import 和 export 的 module。
- HarmonyExportSpecifierDependency 识别 used export 和 unused export
- 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 才能生效。
- 工具类函数尽量以单独的形式输出,不要集中成一个对象或者类,避免打包对象和类未使用的部分。
作者
![]() |
转载自:https://juejin.cn/post/6866747701908733966