當javascript應用體積越來越大時,一個有利於減少體積的辦法是拆分為不同的模塊,伴隨着模塊化的產生,我們也可以進一步的移除多余的代碼,比如那些雖然被應用,但是沒有被實際用到的代碼。tree shaking就是上述說法的一種實現,它通過去除所有引入但是並沒有實際用到的代碼來優化我們的最終打包結果的體積。
比如說,我們有一個工具文件,其中包含一些方法。
// math.js
export function add(a, b) {
console.log("add");
return a + b;
}
export function minus(a, b) {
console.log("minus");
return a - b;
}
export function multiply(a, b) {
console.log("multiply");
return a * b;
}
export function divide(a, b) {
console.log("divide");
return a / b;
}
在我們的應用入口文件中,我們僅僅引入其中某一個方法,比如 add
// index.js
import { add } from "./math";
add(1, 2);
假設我們使用webpack進行打包,下面是結果,我們仍然可以看到所有的方法,雖然我們僅僅想引入add
方法
不過,一旦我們開啟 tree shaking
,就只有我們引入的add
方法會出現在bundle中
tree shaking的原理
盡管90年代就有了tree shaking的概念,但是對於前段來說,tree shaking真正可以使用是在引入 es6-module
后。因為tree shaking 僅僅能分析靜態語法。
在 es6 modules 之前,我們有commonjs規范,可以通過require()
語法引入,但是,require是動態的,意味着我們可以在if - else 中使用它。
var myDynamicModule;
if (condition) {
myDynamicModule = require("foo");
} else {
myDynamicModule = require("bar");
}
上面的是commonjs模塊的語法,不過tree shaking無法使用,因為在程序運行之前無法判斷哪個模塊會被實際應用。也就是說語法分析不能識別。
es6引入了新的完全靜態的語法,使用import
發育,不在支持動態引入。(后來引入了的import()
,返回promise,還是支持動態引入的.)
// not work
if (condition) {
import foo from "foo";
} else {
import bar from "bar";
}
相反,我們只能定義所有的引入在if - else之外的全局環境內,
import foo from "foo";
import bar from "bar";
if (condition) {
// do stuff with foo
} else {
// do stuff with bar
}
除了其他的改進好處,新的語法有效的支持了tree shaking,任何代碼不需要運行就可以通過語法解析判斷出是否真正唄用到,以便進一步減少打包后的體積
tree shaking 甩掉了什么
webpack中的tree shaking,盡可能的甩掉了所有未使用到的代碼,例如,引入了但是沒有實際應用的代碼會被消除。
import { add, multiply } from "./mathUtils";
add(1, 2);
上面的代碼,multiply因為被實際應用,所以會被消除
即使對象上有定義的屬性,但是如果沒有被訪問,也會被移除
// myInfo.js
export const myInfo = {
name: "Ire Aderinokun",
birthday: "2 March"
}
import { myInfo } from "./myInfo.js";
console.log(myInfo.name);
before:
after:
不過 tree shaking 並不能消除所有的代碼,因為 tree shaking 只會處理方法和變量,看下面的例子
// myClass.js
class MyClass {}
MyClass.prototype.saySome = function () {} // 副作用
// 擴展數據的方法
Array.prototype.unique = function () {}
export default MyClass
import MyClass from './myClass';
console.log('index');
這個時候就不會消除類文件myClass,因為會觸發getter、setter,而getter、setter是不透明的,可能會有副作用
詳情參考
也許你會說我們能判斷是否是原生方法,進而進行排除,其實不然,我們可以這樣
let a = 'a';
a += 'rr';
a += 'ay';
一句話就是,因為js本身是動態語言,所以情況太多,處理起來會有風險,tree shaking 的本意是優化,而不是影響,所以不是所有的代碼都可以tree shaking
關於 'side effects'
中文直譯:副作用
什么是副作用:對當前包意外產生任意影響就是副作用
一個典型的具有副作用的形式是 pollfills,因為它在window上增加了各種宿主環境不支持的最新方法,所以不能添加side effects
如何tree shake
webpack目前支持通過設置mode來開啟,比如
module.exports = {
...,
mode: "production",
...,
};
webpack老版本依賴uglifyjs