You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
!function(n){functiont(e){if(r[e])returnr[e].exports;varo=r[e]={i: e,l: !1,exports: {}};returnn[e].call(o.exports,o,o.exports,t),o.l=!0,o.exports}varr={};t.m=n,t.c=r,t.i=function(n){returnn},t.d=function(n,r,e){t.o(n,r)||Object.defineProperty(n,r,{configurable: !1,enumerable: !0,get: e})},t.n=function(n){varr=n&&n.__esModule ? function(){returnn.default} : function(){returnn};returnt.d(r,"a",r),r},t.o=function(n,t){returnObject.prototype.hasOwnProperty.call(n,t)},t.p="",t(t.s=2)}([function(n,t,r){"use strict";t.__esModule=!0,t.default=function(n,t){if(!(ninstanceoft))thrownewTypeError("Cannot call a class as a function")}},function(n,t,r){"use strict";r.d(t,"a",function(){returnu});vare=r(0),o=r.n(e),u=(function(){functionn(){o()(this,n)}n.prototype.toString=function(){return"V6"}}(),function(){functionn(){o()(this,n)}returnn.prototype.toString=function(){return"V8"},n}())},function(n,t,r){"use strict";Object.defineProperty(t,"__esModule",{value: !0});vare=r(0),o=r.n(e),u=r(1),i=function(){functionn(t){o()(this,n),this.engine=t}returnn.prototype.toString=function(){returnthis.engine.toString()+" Sports Car"},n}();console.log(newi(newu.a).toString())}]);
**为什么 ****V6Engine**没有被移除?原因是什么
我们先看整个过程是
babel-loader将模块语法转化成es5,然后保留es module输出
webpack将所有模块整合成bundle
webpack将bundle交给UglifyJS压缩移
这期间 UglifyJS输出如下 warning WARNING in car.prod.bundle.js from UglifyJs Dropping unused function getVersion [car.prod.bundle.js:103,9] Side effects in initialization of unused variable V6Engine [car.prod.bundle.js:79,4]
webpack把sideEffects当成一个优化项,通过optimization.sideEffects配置控制,该配置在mode: production模式下默认开启,只有optimization.sideEffects: true,声明在package.json内的sideEffects or rule.sideEffects字段才会生效
compiler.hooks.compilation.tapnormalModuleFactory.hooks.module.tapconstsideEffects=resolveData.descriptionFileData.sideEffects;module.factoryMeta.sideEffectFree=!sideEffects;normalModuleFactory.hooks.parser.for('javascript/auto').tapparser.hooks.program.tap("SideEffectsFlagPlugin",()=>{sideEffectsStatement=undefined;});parser.hooks.statement.tap({name: "SideEffectsFlagPlugin",stage: -100},statement=>{if(sideEffectsStatement)return;if(parser.scope.topLevelScope!==true)return;switch(statement.type){case"ExpressionStatement":
if(!parser.isPure(statement.expression,statement.range[0])){sideEffectsStatement=statement;}break;case"VariableDeclaration":
case"ClassDeclaration":
case"FunctionDeclaration":
if(!parser.isPure(statement,statement.range[0])){sideEffectsStatement=statement;}break;default:
sideEffectsStatement=statement;break;}});parser.hooks.finish.tap("SideEffectsFlagPlugin",()=>{if(sideEffectsStatement===undefined){parser.state.module.buildMeta.sideEffectFree=true;}else{const{ loc, type }=sideEffectsStatement;moduleGraph.getOptimizationBailout(parser.state.module).push(()=>`Statement (${type}) with side effects in source code at ${formatLocation(loc)}`);}});
后面生成chunk的时候,会根据factoryMeta.sideEffectFree or buildMeta.sideEffectFree标记决定当前chunk是否需要包含该module,最终达到tree shaking效果
// 构建chunkGraphbuildChunkGraph// 根据entry module提取依赖moduleextractBlockModules// 根据当前模块的outgoingConnections获取到当前模块的依赖,并遍历这些依赖forofmoduleGraph.getOutgoingConnections(module)// 获取当前module的状态conststate=connection.getActiveState(runtime);// 调用当前模块的getSideEffectsConnectionState获取当前模块状态refModule.getSideEffectsConnectionState(moduleGraph);getSideEffectsConnectionState(moduleGraph){// 如果设置了sideEffects: false 这里的this.factoryMeta.sideEffectFree=trueif(this.factoryMeta!==undefined){if(this.factoryMeta.sideEffectFree)returnfalse;if(this.factoryMeta.sideEffectFree===false)returntrue;}// 如果没有设置sideEffects,则通过sideEffects插件在webpack解析ast的过程,如果判定模块没有副作用this.buildMeta.sideEffectFree=trueif(this.buildMeta!==undefined&&this.buildMeta.sideEffectFree){if(this._isEvaluatingSideEffects)returnModuleGraphConnection.CIRCULAR_CONNECTION;this._isEvaluatingSideEffects=true;/** @type {ConnectionState} */letcurrent=false;for(constdepofthis.dependencies){conststate=dep.getModuleEvaluationSideEffectsState(moduleGraph);if(state===true){this._isEvaluatingSideEffects=false;returntrue;}elseif(state!==ModuleGraphConnection.CIRCULAR_CONNECTION){current=ModuleGraphConnection.addConnectionStates(current,state);}}this._isEvaluatingSideEffects=false;// When caching is implemented here, make sure to not cache when// at least one circular connection was in the loop abovereturncurrent;}else{// 其它情况,需要包含当前modulereturntrue;}}// 如果模块状态为false,则该模块不会被包含进当前chunkif(state===false)continue;
语雀地址
目录
背景
说到tree shaking,马上想到的是
rollup
率先在javascript
中使用es6
模块才能生效webpack
项目开启了tree shaking好像并没用什么用tree shaking究竟是什么
tree shaking 是一个术语,通常用于描述移除
JavaScript
上下文中的未引用代码(dead-code
)为什么依赖es module
es module规范中
这就决定了,模块的导入导出,不需要运行就能够确定关系,所以tree shaking才能够被运用起来
rollup
的tree shaking在构建npm包的场景已经很成熟,也取得了很好的效果,所以现在关注的重点是,在2022年的今天,webpack
的tree shaking到底有没有用,相比于之前又有了哪些改进,及中间大概的一个历程webpack2 tree shaking
在
wbepack2
之前,webpack2
无法做到两件事情webpack
是借助babel-loader
来将es module转化为webpack
能够识别的commonjs module为了增加这两项主要能力,
webpack
在2017年1月发布第一个正式的2.2.0版本,此版本直接包含了直接处理es6 module,及借助UglifyJS实现tree shaking的能力看一个例子
通过定义类
SportsCar
,我们只使用了V8Engine
,而没有用到V6Engine
。代码 tree shaking 后,我们期望打包结果只包含用到的类和函数。在这个例子中,意味着它只有
V8Engine
和SportsCar
类。打包时不使用编译器(Babel 等)和压缩工具(UglifyJS 等),可以得到如下输出:
Webpack
用注释/*unused harmony export V6Engine*/
将未使用的类和函数标记下来,用/*harmony export (immutable)*/ webpack_exports[“a”] = V8Engine;
来标记用到的。为什么未使用的代码怎么还在?tree shaking 没有生效吗?
背后的原因是:
Webpack
仅仅标记未使用的代码(而不移除),并且不将其导出到模块外。它拉取所有用到的代码,将剩余的(未使用的)代码留给像UglifyJS
这类压缩代码的工具来移除。UglifyJS
读取打包结果,在压缩之后移除未使用的代码。通过这一机制,就可以移除未使用的函数getVersion
和类V6Engine
。这里与rollup
是有差异的UglifyJS
当时是 不支持 ES6及以上语法压缩的。所以需要用Babel
将代码编译为ES5
,然后再用UglifyJS
来清除无用代码。添加babel
配置,并且需要保证babel
不影响模块的输出方式,所以将babel
预设的modules改为false,原样输出es6 module对应的
webpack
配置压缩之后的代码,可以看到函数
getVersion
被移除了,这是我们所预期的,但是类V6Engine
并没有被移除**为什么 **
**V6Engine**
没有被移除?原因是什么我们先看整个过程是
babel-loader
将模块语法转化成es5
,然后保留es module输出webpack
将所有模块整合成bundlewebpack
将bundle交给UglifyJS
压缩移这期间
UglifyJS
输出如下 warningWARNING in car.prod.bundle.js from UglifyJs Dropping unused function getVersion [car.prod.bundle.js:103,9] Side effects in initialization of unused variable V6Engine [car.prod.bundle.js:79,4]
uglifyjs
删除了未使用的函数getVersion
,但是未使用的变量V6Engine
有副作用我们看下
babel
转化成es5
输出的代码在使用
ES5
语法定义类时,类的成员函数会被添加到属性prototype
。UglifyJS
不能够知道它仅仅是类声明,还是其它有副作用的操作,因为UglifyJS
不能做控制流分析。所以最终tree shaking只对函数生效,对类未生效
这里是一些当时关于class tree shaking的issues Webpack repository、UglifyJS repository, 以及解决这个问题的两个思路
es5
,直接输出es6
代码,然后UglifyJS
支持es6+语法压缩UglifyJS
自身支持,还需要babel
、typescipt
等转换器支持,具体issues:Babel、Typescript。针对思路1: 后续的
uglify
直接支持压缩es6+语法针对思路2:
uglify
、babel
、typescript
都做了对应的支持当然
babel
团队提供了第三个思路,就是不使用uglify
压缩代码,babel
自己写一个压缩工具,为什么?因为babel
插件体系本身就支持es6+语法,可以直接复用插件,babel
团队只需要写压缩部分代码所以在当时的条件下,
babel
团队基于babel
的生态做作了一个压缩工具,叫做babili
,现在改名叫babel-minify
,压缩器直接支持es6
语法的压缩,整个压缩流程如下所示之前
ES2015+ code -> Babel es5 -> BabelMinify/Uglify -> Minified ES5 Code
之后
ES2015+ code -> BabelMinify -> Minified ES2015+ Code
遗憾的是
babel-minify
还是0.x的版本,其官方也是不建议在生产环境使用到这个时间节点我们能够得出的结论
webpack
场景下, tree shaking不做具体的代码删减,只做标记webpack
场景下,代码tree shaking 交给了uglifyJS
这样的压缩工具来做babel
团队基于babel
生态做了一个压缩工具tree shaking还存在如下问题
es5
类做tree shakingwebpack4 sideEffects
什么是副作用
在es module模块上下文中,副作用就是在加载该模块的时候会执行一些影响外部模块执行的代码
比如下面的代码
这里的模块
index.js
内未使用a.js
导出的a
变量,反而使用了a
模块内在数组圆形上添加的sum
方法,这些向全局变量上添加属性or方法,以及填充浏览器的行为都是副作用常见的副作用有
console.log
dom
操作,比如document.write
这么看其实副作用代码,貌似也没多大影响,因为项目内的代码都是我们自己写的,我们自己可以控制副作用代码,但是项目不止包含我们自己写的代码,更多的代码是来自第三方npm包,对于第三方npm包内是否有副作用,我们是无法保证的
比如有一个包
big-module
,代码如下所示项目中仅使用
a
,没有使用b
,但是最终的结果是b模块内容也被打包进来了,为什么b
会被保留,原因是b.js
内有副作用代码,当有副作用代码的时候,webpack
无法判断副作用是否能够删除所以最终经过压缩工具的时候虽然b能够被删除,但是b模块的副作用代码会被保留下来
这样无形之中我们项目的代码就变大了,只引用了npm包中的一个方法或者组件,但是额外保留了一些副作用代码
另外看一些更复杂一点的场景,还是上面的类似例子
这里可以看到
re-export
重写导出,a
、b
、c
三个模块,之间是没有互相影响的,如果给webpack
一个标记,就可以精确的排除b
、c
模块,而不需要通过跟踪或者判断是否被使用,这样可以减少bundle尺寸,与优化构建性能如果我们给
c
一些副作用,也就是将b+a
的导出赋值给c
,然后在重新导出c
,那么webpack
在最终处理的时候,虽然外部最终只引用了c
,但是a、b
也会被打进bundle内所以模块中的副作用除了上面列的几点,还有这里的模块重导出,也就是说有模块重导出的场景,其它每个模块导入之后的模块,都有可能对后导入的模块产生副作用,及模块最终导出的时候可能产生新的副作用
为了针对各种副作用的处理,
webpack
选择了sideEffects: false
一种声明式的方式,帮助webpack
快速识别模块副作用,从而更好的实现代码tree shakingsideEffects作用
上面说到了,
webpack4
中引入了新的处理副作用的方式,那就是通过在package.json中声明一个sideEffects
字段来表明npm包或者项目内的模块是否有副作用为什么用声明式,而不是通过程序流程判断的方式,原因是声明式更简单,也有效,将项目与包是否有副作用的控制权交个项目或者包的开发着,而
webpack
在构建的时候只需要根据声明来判断怎么处理副作用,而不用通过复杂的程序分析流程去判断模块是否有副作用,这样可以大大减少构建时间webpack
把sideEffects
当成一个优化项,通过optimization.sideEffects
配置控制,该配置在mode: production
模式下默认开启,只有optimization.sideEffects: true
,声明在package.json
内的sideEffects
orrule.sideEffects
字段才会生效三种方式控制副作用
看一个具体的例子
big-module
与big-module-with-flag
是两个npm包,代码内容一模一样,唯一不同的是big-module-with-flag
package.json
内有"sideEffects": false
app内有如下模块,且使用关系如下图所示
mode: production
,项目package.json
不设置sideEffects
构建,不进行压缩,构建结果如下图所示mode: production
,项目package.json
设置sideEffects: false
构建,不进行压缩,构建结果如下图所示从上面结果,我们可以得出如下结论
sideEffects:true
,模块的export
没有一个被父模块使用,只要模块内有副作用,那么模块内所有代码就会被保留,比如./no-used-effects.js
,`big-module/index.js``sideEffects:false
,且模块不论是否有副作用代码,只要模块的export
没有一个被父模块使用,则整个模块会被删除,比如./no-used-effects.js
,big-module-with-flag/index.js
export
有没有被使用,是根据父模块导入的变量来进行判断,注意这里的判断是简单的通过引用关系判断,没有深入分析导入的变量是否被使用,比如const newGetVersion = getVersion
这行代码,其实newGetVersion
在app模块内是没有被使用的到这里有几个疑问?
webpack
是怎么判断模块export
的变量有没有被使用?webpack
是怎么判断模块是否有副作用?webpack
生成代码的时候是怎样排除无export
被使用,且无副作用的模块?下面以
webpack
(5.73.0)版本为案例,看下webpack
内部是怎么实现的webpack usedExport 实现原理
optimization.usedExport
标记模块导出是否被使用,用于压缩插件直接去掉未使用的导出,具体输出如下所示标记
export
是否被使用,在FlagDependencyExportsPlugin
与FlagDependencyUsagePlugin
插件内处理FlagDependencyExportsPlugin插件
FlagDependencyExportsPlugin
插件内的主流程为:finishModules hook
内开始处理模块依赖,此时所有的模块都已经转换成对应的Module
实例Module
实例,通过moduleGraph
获取Module
实例的exportsInfo
对象processDependenciesBlock
方法内遍历Module.dependencies
,将dependecy
存储到exportsSpecsFromDependencies
map
上exportsSpecsFromDependencies
,将depenecy
转化成exportInfo
,并存储在exportsInfo
内ModuleGraphModule.exports
上,已经保留了每个模块的export
对象信息,用于下一步的判断模块内的export
是否有被使用FlagDependencyUsagePlugin
FlagDependencyUsagePlugin
插件的主流程为:optimizeDependencies hook
内开始处理,注意这个hook
在finnishModules hook
之后dependencies
(注意这里模块在生成dependencies
的时候,就已经过滤了未使用的导入变量)dependecy
获取ModuleGraphConnection
,然后通过ModuleGraphConnection.getActiveState
判断当前模块是否有副作用,如果无副作用直接过滤getDependencyReferencedExports
方法获取当前模块使用了的子应用的export
变量,并将子模块与referencedExports
存储在一个map
对象map
对象,通过父模块内已使用的export
对象,与子模块的exportsInfo
对象,判断子模块的export
是否被父模块使用,如果被父模块使用,则将子模块对应的exportInfo._useInRuntime
进行标记,用于最终生成代码时的判断,然后将当前子模块推入到队列,重复上述步骤,直到所有的子模块的export
都判断完成代码最终生成时的主流程为:
sourceModule
方法内遍历module.dependencies
sourceDependency
方法内moduleGraph.getExportsInfo(module).getUsedName(dep.name, runtime);
通过ModuleGraphModule.exports
对应exportInfo._usedInRuntime
进行判断当前模块export
是否被使用webpack sideEffect 实现原理
副作用的处理在
SideEffectsFlagPlugin
插件内SideEffectsFlagPlugin
插件的主流程为:normalModuleFactory module hook
内进行模块副作用标记,这时候Module
实例刚创建package.json sideEffects: false
,对用的模块factoryMeta.sideEffectFree = true
normalModuleFactory parse hook
内针对不同的语句,来判断当前模块是否有副作用,如果当前模块没有副作用,则buildMeta.sideEffectFree=true
,否则将有副作用的语句,存储到module.Bailout
属性内,便于输出日志后面生成
chunk
的时候,会根据factoryMeta.sideEffectFree
orbuildMeta.sideEffectFree
标记决定当前chunk
是否需要包含该module
,最终达到tree shaking效果生成
chunk
时过滤模块的主流程为:entry
模块开始遍历,通过moduleGraph.getOutgoingConnections
获取entry
模块导入的子模块OutgoingConnections
,通过getActiveState
方法判断当前模块是否有副作用chunk
chunk
的阶段来进行判断在回过头来看之前的几个疑问?
webpack
是怎么判断模块export
的变量有没有被使用?export
被使用webpack
是怎么判断模块是否有副作用?package.json
内的sideEffects
标识,以及webpack
在解析代码成ast
的过程中判断模块是否有副作用的webpack
生成代码的时候是怎样排除无export
被使用,且无副作用的模块?chunk
的时候,通过判断当前的副作用标识来决定当前模块是否要保留压缩插件
Uglify-js 压缩扛霸子
最早的
uglify
,仅支持es5
语法,不支持新的es6+语法,所以在uglify
的基础上出了一个uglify-es
,后面又丢弃了uglify-es
,并重新回到uglify-js
,现在的uglify3.x是支持直接压缩es6+语法的为什么会有这么一个过程,原因是
uglify
缺少维护者,导致对es6+语法的支持不够,直到现在又重新维护,所以我们在使用webpack
压缩代码的时候,会经历过使用uglify
压缩,然后又是uglify-es
压缩,现在又是terser
进行压缩Terser webpack内置压缩工具
terser
是从uglify3.x fork过来之后单独维护的压缩工具,为什么会fork过来,还是因为当时的uglify-es
没有长期维护着,经常有一些bug没有修复,所以就fork了一份,单独维护所以
terser
的api与unglify3.x版本的api几乎一致terser-webpack-plugin
terser-webpack-plugin
插件已经被webpack5
内置,开箱即用terser-webpack-plugin
封装了多种压缩工具我们在项目使用的时候可以根据项目灵活选择压缩工具,以便提升压缩速度
关键compress参数
dead_code
unused
最佳实践
虽然
webpack4
及以上在webpack2
的基础上添加了sideEffects
标识来帮助tree shaking,但是受限于js的灵活性与模块的复杂性,还是有很多没有解决的副作用场景,使得最终的tree shaking效果不够完美,所以要想更好的tree shaking效果,我们可以人为的使用一些手段去提高tree shaking的效果配置mainFields优先级
禁止babel转换模块语法
使用自带tree shaking的包
比如使用
lodash-es
替换lodash
等不过并不是所有的npm包都还有tree shaking的优化空间,比如
vue
、react
等提供的min
版本已经是最小功能集的版本避免无意义的赋值
从之前的例子可以看到,
webpack
判断模块的export
是否被使用,只是简单的判断了是否有引用关系,没有进行程序流程分析,所以这就导致了明明可以被摇树的包,最终还保留了下来优化导出方式
webpack
的tree shaking依赖的是模块的export
,所以下面这样的导出方式,虽然只使用了一个,但最终都会被保留下来所以实际开发中,应该尽量保持导出值颗粒度和原子性,上例代码的优化版本
npm包配置sideEffects
如果npm不是用于
polyfill
,可以大胆的在package.json
内设置"sideEffects": false
如果包内有些模块确定是有副作用,可以设置数组形式,以
@abc/yked
为例该npm包内
.less
与index.js
文件是有副作用的,这些副作用需要保留,其它模块都是可以认为没有副作用的如果包确实是有副作用的,那么久不需要设置了,不设置就默认
sideEffects: true
已添加sideEffects的npm包
等等还有很多现在使用量非常大的包
新项目配置sideEffects
其实对于一个项目的组成,主要由两部分组成,一部分是自己写的代码,一部分是第三方npm包,所以对项目配置
sideEffects
也可以如果是新项目推荐如下配置
如果是旧项目不推荐在
package.json
内设置sideEffects
字段如果想要给第三方npm包添加无副作用标识,可以通过自己写一个插件,针对需要的模块设置副作用的值
总结
webpack4
及以上的tree shaking包含两部分webpack
标记unused
,然后由压缩插件terser
等直接删除未使用的这部分代码(可以配置压缩插件参数不删除)webpack
sideEffects
是模块级别,根据模块的export
变量是否使用来判断模块是否需要保留sideEffects: false
标记的作用就是告诉webpack
当前项目 ornpm
包内的模块是没有副作用的,就是有副作用,也当成没有副作用处理参考链接
Everything you never wanted to know about side effects
What Does Webpack 4 Expect From A Package With sideEffects: false
Tree Shaking
你的Tree-Shaking并没什么卵用
Tree-Shaking性能优化实践 - 原理篇
The text was updated successfully, but these errors were encountered: