Webpack Tree shaking 深入探究
栏目: JavaScript · 发布时间: 6年前
内容简介:App往往有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过Tree shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。CommonJS的模块ES2015(ES6)的模块
App往往有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过Tree shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。
模块
CommonJS的模块 require
modules.exports
, exports
var my_lib; if (Math.random()) { my_lib = require('foo'); } else { my_lib = require('bar'); } module.exports = xx 复制代码
ES2015(ES6)的模块 import
, export
// lib.js export function foo() {} export function bar() {} // main.js import { foo } from './lib.js'; foo(); 复制代码
Tree shaking的原理
关于Tree shaking的原理,在 Tree Shaking性能优化实践 - 原理篇 已经说的比较清楚,简单来说。
Tree shaking的本质是消除无用的JavaScript代码。 因为ES6模块的出现,ES6模块依赖关系是确定的,`和运行时的状态无关`,可以进行可靠的静态分析, 这就是Tree shaking的基础。 复制代码
支持Tree-shaking的工具
- Webpack/UglifyJS
- rollup
- Google closure compiler
今天,我们来看一下Webpack的Tree shaking做了什么
Webpack Tree shaking
Tree shaking到底能做哪些事情??
1.Webpack Tree shaking从ES6顶层模块开始分析,可以清除未使用的模块
从官网的例子来看 代码 :
//App.js import { cube } from './utils.js'; cube(2); //utils.js export function square(x) { console.log('square'); return x * x; } export function cube(x) { console.log('cube'); return x * x * x; } 复制代码
result: square的代码被移除
function(e, t, r) { "use strict"; r.r(t), console.log("cube") } 复制代码
2.Webpack Tree shaking会对多层调用的模块进行重构,提取其中的代码,简化函数的调用结构
//App.js import { getEntry } from './utils' console.log(getEntry()); //utils.js import entry1 from './entry.js' export function getEntry() { return entry1(); } //entry.js export default function entry1() { return 'entry1' } 复制代码
result: 简化后的代码如下
//摘录核心代码 function(e, t, r) { "use strict"; r.r(t), console.log("entry1") } 复制代码
3.Webpack Tree shaking不会清除IIFE(立即调用函数表达式)
IIFE是什么??IIFE in MDN
//App.js import { cube } from './utils.js'; console.log(cube(2)); //utils.js var square = function(x) { console.log('square'); }(); export function cube(x) { console.log('cube'); return x * x * x; } 复制代码
result: square和cude都存在
function(e, t, n) { "use strict"; n.r(t); console.log("square"); console.log(function(e) { return console.log("cube"), e * e * e }(2)) } 复制代码
这里的问题会是为什么不会清除IIFE?在 你的Tree-Shaking并没什么卵用 中有过分析,里面有一个例子比较好,见下文
原因很简单: 因为IIFE比较特殊,它在被翻译时(JS并非编译型的语言)就会被执行,Webpack不做程序流分析,它不知道IIFE会做什么特别的事情,所以不会删除这部分代码
比如:
var V8Engine = (function () { function V8Engine () {} V8Engine.prototype.toString = function () { return 'V8' } return V8Engine }()) var V6Engine = (function () { function V6Engine () {} V6Engine.prototype = V8Engine.prototype // <---- side effect V6Engine.prototype.toString = function () { return 'V6' } return V6Engine }()) console.log(new V8Engine().toString()) 复制代码
result:
输出V6,而并不是V8 复制代码
如果V6这个IIFE里面再搞一些全局变量的声明,那就当然不能删除了。
4.Webpack Tree shaking对于IIFE的返回函数,如果未使用会被清除
当然Webpack也没有那么的傻,如果发现IIFE的返回函数没有地方调用的话,依旧是可以被删除的
//App.js import { cube } from './utils.js'; console.log(cube(2)); //utils.js var square = function(x) { console.log('square'); return x * x; }(); function getSquare() { console.log('getSquare'); square(); } export function cube(x) { console.log('cube'); return x * x * x; } 复制代码
result: 结果如下
function(e, t, n) { "use strict"; n.r(t); console.log("square"); <= square这个IIFE内部的代码还在 console.log(function(e) { return console.log("cube"), e * e * e <= square这个IIFEreturn的方法因为getSquare未被调用而被删除 }(2)) } 复制代码
5.Webpack Tree shaking结合第三方包使用
//App.js import { getLast } from './utils.js'; console.log(getLast('abcdefg')); //utils.js import _ from 'lodash'; <=这里的引用方式不同,会造成bundle的不同结果 export function getLast(string) { console.log('getLast'); return _.last(string); } 复制代码
result: 结果如下
import _ from 'lodash'; Asset Size bundle.js 70.5 KiB import { last } from 'lodash'; Asset Size bundle.js 70.5 KiB import last from 'lodash/last'; <=这种引用方式明显降低了打包后的大小 Asset Size bundle.js 1.14 KiB 复制代码
Webpack Tree shaking做不到的事情
在 体积减少80%!释放webpack tree-shaking的真正潜力 一文中提到了,Webpack Tree shaking虽然很强大,但是依旧存在缺陷
//App.js import { Add } from './utils' Add(1 + 2); //utils.js import { isArray } from 'lodash-es'; export function array(array) { console.log('isArray'); return isArray(array); } export function Add(a, b) { console.log('Add'); return a + b } 复制代码
result: 不该导入的代码
这个`array`函数未被使用,但是lodash-es这个包的部分代码还是会被build到bundle.js中 复制代码
可以使用这个插件 webpack-deep-scope-analysis-plugin 解决
小结
如果要 更好
的使用Webpack Tree shaking,请满足:
- 使用ES2015(ES6)的模块
- 避免使用IIFE
- 如果使用第三方的模块,可以尝试直接从文件路径引用的方式使用(这并不是最佳的方式)
import { fn } from 'module'; => import fn from 'module/XX'; 复制代码
Babel带来的问题1-语法转换(Babel6)
以上的所有示例都没有使用Babel进行处理
,但是我们明白在真实的项目中,Babel对于我们还是必要的。那么如果使用了Babel会带来什么问题呢?(以下讨论建立在 Babel6
的基础上)
我们看 代码 :
//App.js import { Apple } from './components' const appleModel = new Apple({ <==仅调用了Apple model: 'IphoneX' }).getModel() console.log(appleModel) //components.js export class Person { constructor ({ name, age, sex }) { this.className = 'Person' this.name = name this.age = age this.sex = sex } getName () { return this.name } } export class Apple { constructor ({ model }) { this.className = 'Apple' this.model = model } getModel () { return this.model } } //webpack.config.js const path = require('path'); module.exports = { entry: [ './App.js' ], output: { filename: 'bundle.js', path: path.resolve(__dirname, './build'), }, module: {}, mode: 'production' }; 复制代码
result: 结果如下
function(e, t, n) { "use strict"; n.r(t); const r = new class { constructor({ model: e }) { this.className = "Apple", this.model = e } getModel() { return this.model } }({ model: "IphoneX" }).getModel(); console.log(r) } //仅有Apple的类,没有Person的类(Tree shaking成功) //class还是class,并没有经过语法转换(没有经过Babel的处理) 复制代码
但是如果加上Babel(babel-loader)的处理呢?
//App.js和component.js保持不变 //webpack.config.js const path = require('path'); module.exports = { entry: [ './App.js' ], output: { filename: 'bundle.js', path: path.resolve(__dirname, './buildBabel'), }, module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['env'] } } } ] }, mode: 'production' }; 复制代码
result: 结果如下
function(e, n, t) { "use strict"; Object.defineProperty(n, "__esModule", { value: !0 }); var r = function() { function e(e, n) { for(var t = 0; t < n.length; t++) { var r = n[t]; r.enumerable = r.enumerable || !1, r.configurable = !0, "value" in r && (r.writable = !0), Object.defineProperty(e, r.key, r) } } return function(n, t, r) { return t && e(n.prototype, t), r && e(n, r), n } }(); function o(e, n) { if(!(e instanceof n)) throw new TypeError("Cannot call a class as a function") } n.Person = function() { function e(n) { var t = n.name, r = n.age, u = n.sex; o(this, e), this.className = "Person", this.name = t, this.age = r, this.sex = u } return r(e, [{ key: "getName", value: function() { return this.name } }]), e }(), n.Apple = function() { function e(n) { var t = n.model; o(this, e), this.className = "Apple", this.model = t } return r(e, [{ key: "getModel", value: function() { return this.model } }]), e }() } //这次不仅Apple类在,Person类也存在(Tree shaking失败了) //class已经被babel处理转换了 复制代码
结论:Webpack的Tree Shaking有能力除去导出但没有使用的代码块,但是结合Babel(6)使用之后就会出现问题
那么我们看看Babel到底干了什么, 这是被Babel6处理的代码
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); //_createClass本质上也是一个IIFE var _createClass = function() { function defineProperties(target, props) { for(var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function(Constructor, protoProps, staticProps) { if(protoProps) defineProperties(Constructor.prototype, protoProps); if(staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if(!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } //Person本质上也是一个IIFE var Person = exports.Person = function () { function Person(_ref) { var name = _ref.name, age = _ref.age, sex = _ref.sex; _classCallCheck(this, Person); this.className = 'Person'; this.name = name; this.age = age; this.sex = sex; } _createClass(Person, [{ <==这里调用了另一个IIFE key: 'getName', value: function getName() { return this.name; } }]); return Person; }(); 复制代码
从最开始,我们就清楚Webpack Tree shaking是不处理IIFE的,所以这里即使没有调用Person类在bundle中也存在了Person类的代码。
我们可以设定使用 loose: true
来使得Babel在转化时使用宽松的模式,但是这样也仅仅只能去除 _createClass
,Person本身依旧存在
//webpack.config.js { loader: 'babel-loader', options: { presets: [["env", { loose: true }]] } } 复制代码
result: 结果如下
function(e, t, n) { "use strict"; function r(e, t) { if(!(e instanceof t)) throw new TypeError("Cannot call a class as a function") } t.__esModule = !0; t.Person = function() { function e(t) { var n = t.name, o = t.age, u = t.sex; r(this, e), this.className = "Person", this.name = n, this.age = o, this.sex = u } return e.prototype.getName = function() { return this.name }, e }(), t.Apple = function() { function e(t) { var n = t.model; r(this, e), this.className = "Apple", this.model = n } return e.prototype.getModel = function() { return this.model }, e }() } 复制代码
Babel6的讨论
Class declaration in IIFE considered as side effect
详见: github.com/mishoo/Ugli…
总结:
- Uglify doesn't perform program flow analysis. but rollup did(Uglify不做程序流的分析,但是rollup做了)
- Variable assignment could cause an side effect(变量的赋值可能会引起副作用)
-
Add some
/*#__PURE__*/
annotation could help with it(可以尝试添加注释/*#__PURE__*/
的方式来声明一个无副作用的函数,使得Webpack在分析处理时可以过滤掉这部分代码)
关于第三点:添加 /*#__PURE__*/
,这也是Babel 7
的执行行为, 这是被Babel7处理的代码
var Person = /*#__PURE__*/ <=这里添加了注释 function() { function Person(_ref) { var name = _ref.name, age = _ref.age, sex = _ref.sex; _classCallCheck(this, Person); this.className = 'Person'; this.name = name; this.age = age; this.sex = sex; } _createClass(Person, [{ key: "getName", value: function getName() { return this.name; } }]); return Person; }(); exports.Person = Person; 复制代码
所以,在Babel7的运行环境下,经过Webpack的处理是可以过滤掉这个未使用的Person类的。
Babel带来的问题2-模块转换(Babel6/7)
我们已经清楚,CommonJS模块和ES6的模块是不一样的,Babel在处理时默认将所有的模块转换成为了 exports
结合 require
的形式,我们也清楚Webpack是基于ES6的模块才能做到最大程度的Tree shaking的,所以我们在使用Babel时,应该将Babel的这一行为关闭,方式如下:
//babel.rc presets: [["env", { module: false } ]] 复制代码
但这里存在一个问题:什么情况下我们该关闭这个转化?
如果我们都在一个App中,这个module的关闭是没有意义的,因为如果关闭了,那么打包出来的bundle是没有办法在浏览器里面运行的(不支持import)。所以这里我们应该在App依赖的某个功能库打包时去设置。
比如:像 lodash/lodash-es
, redux
, react-redux
, styled-component
这类库都同时存在ES5和ES6的版本
- redux - dist - es - lib - src ... 复制代码
同时在packages.json中设置入口配置,就可以让Webpack优先读取ES6的文件 eg: Redux ES 入口
//package.json "main": "lib/redux.js", "unpkg": "dist/redux.js", "module": "es/redux.js", "typings": "./index.d.ts", 复制代码
Webpack Tree shaking - Side Effect
在官方文档中提到了一个sideEffects的标记,但是关于这个标记的作用,文档详述甚少,甚至运行 官方给了例子 ,在最新的版本的Webpack中也无法得到它解释的结果,因此对这个标记的用法存在更多的疑惑。读完 Webpack中的sideEffects到底该怎么用? 这篇大致会对做了什么?怎么用? 有了基本的认知,我们可以接着深挖
Tree shaking到底做了什么
Demo1:
//App.js import { a } from 'tree-shaking-npm-module-demo' console.log(a); //index.js export { a } from "./a"; export { b } from "./b"; export { c } from "./c"; //a.js export var a = "a"; //b.js export var b = "b"; //c.js export var c = "c"; 复制代码
result: 仅仅留下了a的代码
function(e, t, r) { "use strict"; r.r(t); console.log("a") } 复制代码
Demo2:
//App.js import { a } from 'tree-shaking-npm-module-demo' console.log(a); //index.js export { a } from "./a"; export { b } from "./b"; export { c } from "./c"; //a.js export var a = "a"; //b.js (function fun() { console.log('fun'); })() window.name = 'name' export var b = "b"; //c.js export var c = "c"; 复制代码
result: 留下了a的代码,同时还存在b中的代码
function(e, n, t) { "use strict"; t.r(n); console.log("fun"), window.name = "name"; console.log("a") } 复制代码
Demo3: 添加sideEffects标记
//package.json { "sideEffects": false, } 复制代码
result: 仅留下了a的代码,b模块中的所有的副作用的代码被删除了
function(e, t, r) { "use strict"; r.r(t); console.log("a") } 复制代码
综上:参考 What Does Webpack 4 Expect From A Package With sideEffects: false
中 @asdfasdfads(那个目前只有三个赞)
的回答
实际上:
The consensus is that "has no sideEffects" phrase can be decyphered as "doesn't talk to things external to the module at the top level". 译为: "没有副作用"这个短语可以被解释为"不与顶层模块以外的东西进行交互"。 复制代码
在Demo3中,我们添加了 "sideEffects": false
也就意味着:
1.在b模块中虽然有一些副作用的代码(IIFE和更改全局变量/属性的操作),但是我们不认为删除它是有风险的
2.模块被 引用过
(被其他的模块import过或重新export过)
情况A //b.js (function fun() { console.log('fun'); })() window.name = 'name' export var b = "b"; //index.js import { b } from "./b"; 分析: b模块一旦被import,那么其中的代码会在翻译时执行 情况B //b.js (function fun() { console.log('fun'); })() window.name = 'name' export var b = "b"; //index.js export { b } from "./b"; 分析: According to the ECMA Module Spec, whenever a module reexports all exports (regardless if used or unused) need to be evaluated and executed in the case that one of those exports created a side-effect with another. b模块一旦被重新re-export,根据ECMA模块规范,每当模块重新导出所有导出(无论使用或未使用)时,都需要对其中一个导出与另一个导出产生副作用的情况进行评估和执行 情况C //b.js (function fun() { console.log('fun'); })() window.name = 'name' export var b = "b"; //index.js //没有import也没有export 分析: 没用的当然没有什么影响 复制代码
只要满足以上两点:我们就可以根据情况安全的添加这个标记来通知Webpack可以安全的删除这些无用的代码。 当然如果你的代码确实有一些副作用,那么可以改为提供一个数组:
"sideEffects": [ "./src/some-side-effectful-file.js" ] 复制代码
总结:
如果想利用好Webpack的Tree shaking,需要对自己的项目进行一些改动。 建议:
1.对第三方的库:
-
团队的维护的:视情况加上
sideEffects
标记,同时更改Babel配置来导出ES6模块
- 第三方的:尽量使用提供ES模块的版本
2.工具:
- 升级Webpack到4.x
- 升级Babel到7.x
参考
- 你的Tree-Shaking并没什么卵用: juejin.im/post/5a5652…
- Tree-Shaking性能优化实践 - 原理篇: juejin.im/post/5a4dc8…
- Tree-Shaking性能优化实践 - 实践篇: juejin.im/post/5a4dca…
- segmentfault.com/a/119000001…
- 使用Tree-Shaking: www.xbhub.com/wiki/webpac…
- 体积减少80%!释放webpack tree-shaking的真正潜力: juejin.im/post/5b8ce4…
- webpack 如何通过作用域分析消除无用代码: vincentdchan.github.io/2018/05/bet… ( github.com/vincentdcha… )
- 今天,你升级Webpack2了吗? www.aliued.com/?p=4060
- Webpack中文网站: www.webpackjs.com/guides/prod…
-
Webpack中的
sideEffects
到底该怎么用? juejin.im/post/5b4ff9…
以上所述就是小编给大家介绍的《Webpack Tree shaking 深入探究》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 深入探究 Kubernetes:初识容器
- Flutter进阶:深入探究 TextField
- 深入探究 6 个 React 诡异现象
- 深入探究ES6之模块系统
- 深入探究 Objective-C 对象的底层原理
- 深入探究immutable.js的实现机制(二)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
离散数学及其应用
Kenneth H.Rosen / 机械工业出版社 / 2012-11 / 99.00元
本书是介绍离散数学理论和方法的经典教材,已经成为采用率最高的离散数学教材,被美国众多名校用作教材,获得了极大的成功。中文版也已被国内大学广泛采用为教材。作者参考使用教师和学生的反馈,并结合自身对教育的洞察,对第7版做了大量的改进,使其成为更有效的教学工具。. 本书可作为1至2个学期的离散数学课入门教材,适用于数学,计算机科学。计算机工程.信息技术等专业的学生。 本书特点 实例:书中有8......一起来看看 《离散数学及其应用》 这本书的介绍吧!