时下火热的 Vue.js 3.0 从源码、性能和语法 API 三个大的方面对框架进行了优化。其中,在性能的优化中,对于源码体积的优化,主要体如今移除了一些冷门 Feature(好比 filter、inline-template) 并引入了 Tree-Shaking 减小打包体积。自从 rollup 提出这个术语以来,往往谈及打包性能优化,几乎都有 Tree-Shaking 的一席之地,因此了解 Tree-Shaking 的原理是颇有必要的。html
阅读完本文,你能够 Get 如下问题的答案,node
业界知名的模块打包器 rollup.js 的做者 Rich Harris 在 2015 年 12 月的一篇博客 《Tree-shaking versus dead code elimination》中首次提到了 Tree-Shaking 的概念,随后 Webpack 2 的正式版本内置支持了 ECMAScript 2015 模块,增长了对 Tree-Shaking 的支持,而在更早前,Google 推出的开发者工具 Closure Compiler 也在作相似的事情。webpack
I’ve been working (albeit sporadically of late, admittedly) on a tool called Rollup, which bundles together JavaScript modules. One of its features is tree-shaking, by which I mean that it only includes the bits of code your bundle actually needs to run.git
Rich Harris 在文中提到 Tree-Shaking 是为了 Dead code elimination,这是编译器原理中常见的一种编译优化技术,简单来讲就是消除无用代码(Dead code)。那么什么是 Dead code
呢?es6
Dead code,也叫死码,无用代码,它的范畴主要包含了如下两点,github
咱们尝试经过一些 JavaScript 代码片断来理解它。web
首先,不会被运行到的代码很好理解,好比在函数 return
语句后的代码,npm
function foo() {
return 'foo';
var bar = 'bar'; // 函数已经返回了,这里的赋值语句永远不会执行
}
复制代码
或者不会执行的假值条件语句块,编程
if(0) {
// 这个条件判断语句块内部的代码永远不会执行
}
复制代码
而 Dead Variables
常见的像一些未使用的变量,api
function add(a, b) {
let c = 1; // unused variable 在这里能够被看做死码
return a + b;
}
复制代码
须要注意的是,模块若是未使用也能够看做 Dead code
,好比下面的 bar
模块,
// foo.js
function foo() {
console.log('foo');
}
export default foo;
// bar.js
function bar() {
console.log('bar');
}
export default bar;
// index.js
import foo from './foo.js';
import bar from './bar.js';
foo();
// 这里入口文件虽然引用了模块 bar,可是没有使用,模块 bar 也能够被看做死码
复制代码
Dead code 咱们知道了,那么什么是 Tree-Shaking 呢?
在传统的静态编程语言编译器中,编译器能够判断出某些代码根本不影响输出,咱们能够借助编译器将 Dead Code
从 AST
(抽象语法树)中删除,但 JavaScript 是动态语言,编译器不能帮助咱们完成死码消除,咱们须要本身实现 Dead code elimination
。
而咱们说的 Tree-Shaking,就是 Dead code elimination 的一种实现,它借助于 ECMAScript 6 的模块机制原理,更多关注的是对无用模块的消除,消除那些引用了但并无被使用的模块。
这里为了更好地理解 Tree-Shaking 的原理,咱们须要先了解 ES6 的模块机制。
JavaScript 的模块化经历一个漫长的发展历程,咱们知道刚开始 JavaScript 是没有模块的概念的,最初咱们只能借助 IIFE 尽可能减小对全局环境的污染,后来社区出现了用于浏览器端的以 RequireJS 为表明的 AMD 规范和以 Sea.js 为表明的 CMD 规范,服务器端也出现了 CommonJS 规范,再后来 JavaScript 原生引入了 ES Module,取代社区方案成为浏览器端一致的模块解决方案。
ES Module 如今也能够用于服务器,Node.js 在 v12.0.0
版本实现了对 ES Module 的支持。
对于 ES Module 基础语法不了解的能够参考下面的文章,咱们接下来主要理解它的机制,
对比是理解知识很是有效的一种手段。咱们经过对比 ES Module 与 CommonJS 的区别来理解 ES Module 的模块机制,它们的区别主要体如今模块的输出和执行上,
因此 ES Module 最大的特色就是静态化,在编译时就能肯定模块的依赖关系,以及输入和输出的值,这意味着什么?意味着模块的依赖关系是肯定的,和运行时的状态无关,能够进行可靠的静态分析,正是基于这个基础,才使得 Tree-Shaking 成为可能,这也是为何 rollup 和 Webpack 2 都要用 ES6 Module 语法才能支持 Tree-Shaking。
了解原理后,接下来咱们来看下如何实现 Tree-Shaking。
借助静态模块分析,Tree-Shaking 实现的大致思路:借助 ES6 模块语法的静态结构,经过编译阶段的静态分析,找到没有引入的模块并打上标记,而后在压缩阶段利用像 uglify-js
这样的压缩工具删除这些没有用到的代码。
是这样吗?接下来咱们以 webpack
为例,验证下上述思路。
初始化项目安装最新的 webpack
和 webpack-cli
,笔者写这篇文章时最新的版本是 v5.35.1
,
$ mkdir tree-shaking && cd tree-shaking
$ npm init -y
$ npm i webpack webpack-cli -D
复制代码
添加一个简单的配置文件和一个 math
模块,这里咱们只引用 math
模块的 cube
函数,
// webpack.config.js
const path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
},
optimization: {
// 开启 usedExports 收集 Dead code 相关的信息
usedExports: true,
},
};
// src/math.js
export function square(x) {
return x * x;
}
export function cube(x) {
var a, b, c; // 这里引入了三个未使用的变量做为 Dead code 的一种
return x * x * x;
}
// src/index.js
import { cube } from "./math.js";
function component() {
var element = document.createElement("pre");
element.innerHTML = "5 cubed is equal to " + cube(5);
return element;
}
document.body.appendChild(component());
复制代码
运行打包命令,定位到 bundle.js
中 math
模块打包后代码,
/***/ "./src/math.js":
/*!*********************!*\ !*** ./src/math.js ***! \*********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"cube\": () => (/* binding */ cube)\n/* harmony export */ });\n/* unused harmony export square */\nfunction square(x) {\r\n return x * x;\r\n}\r\n\r\nfunction cube(x) {\r\n var a, b, c;\r\n return x * x * x;\r\n}\r\n\n\n//# sourceURL=webpack://tree-shaking/./src/math.js?");
/***/ })
复制代码
为了方便阅读,咱们将 eval
函数内的换行符去掉,简单整理下格式,
/* harmony export */
__webpack_require__.d(__webpack_exports__, {
/* harmony export */
cube: () => /* binding */ cube /* harmony export */,
});
/* unused harmony export square */
function square(x) {
return x * x;
}
function cube(x) {
var a, b, c;
return x * x * x;
}
复制代码
能够看到,__webpack_exports__
只导出了 cube
函数,而没有使用的 square
函数没有被导出,并打上了 /* unused harmony export square */
的注释标识,可是 square
函数声明以及 cube
函数中未使用的变量声明 a, b, c
仍是被打包了。这印证了咱们以前推测的 webpack 能够经过 Tree-Shaking 找到没有引入的模块,并不会删除 Dead code。
接着咱们将 mode
切换到 production
以启用 uglify-js
进行压缩,而后再次运行打包命令,
(() => {
"use strict";
var e, t;
document.body.appendChild(
(((t = document.createElement("pre")).innerHTML =
"5 cubed is equal to " + (e = 5) * e * e),
t)
);
})();
复制代码
结果和咱们预期一致,uglify-js
在压缩的同时去除了 Dead code
,包括,
square
函数a, b, c
咱们也能够单独引入 uglify-js
来验证这一点,
// math.js
function cube(x) {
var a, b, c;
return x * x * x;
}
// minify.js
const fs = require("fs");
const UglifyJS = require("uglify-js");
const code = fs.readFileSync("math.js", "utf-8");
const result = UglifyJS.minify(code, {
compress: {
dead_code: true, // dead_code 默认为 true
},
});
console.log(result.code); // function cube(n){return n*n*n}
复制代码
咱们从 Tree-Shaking 的起源切入,了解了它是 Dead code elimination 的一种实现,而后拓展学习了什么是 Dead Code,接着回顾了 JavaScript 模块化的发展史,正是由于 ES Module 的静态结构,使得模块级别的 Tree-Shaking 实现成为可能。最后以打包工具 webpack 为例,结合 uglify-js 压缩工具,解释了 Tree-Shaking 的实现原理。
本文首发于个人 博客,才疏学浅,不免有错误,文章有误之处还望不吝指正!
若是有疑问或者发现错误,能够在相应的 issues 进行提问或勘误
若是喜欢或者有所启发,欢迎 star,对做者也是一种鼓励
(完)