Tree-Shaking 是一种基于 ES Module 规范的 Dead Code Elimination 技术,它会在运行过程中静态分析模块之间的导入导出,确定 ESM 模块中哪些导出值未曾其它模块使用,并将其删除,以此实现打包产物的优化。
Tree Shaking 较早前由 Rich Harris 在 Rollup 中率先实现,Webpack 自 2.0 版本开始接入,至今已经成为一种应用广泛的性能优化手段。
在 Webpack 中,启动 Tree Shaking 功能必须同时满足三个条件:
optimization.usedExports
为 true
,启动标记功能mode = production
optimization.minimize = true
optimization.minimizer
数组例如:
// webpack.config.js
module.exports = {
entry: "./src/index",
mode: "production",
devtool: false,
optimization: {
usedExports: true,
},
};
在 CommonJs、AMD、CMD 等旧版本的 JavaScript 模块化方案中,导入导出行为是高度动态,难以预测的,例如:
if(process.env.NODE_ENV === 'development'){
require('./bar');
exports.foo = 'foo';
}
而 ESM 方案则从规范层面规避这一行为,它要求所有的导入导出语句只能出现在模块顶层,且导入导出的模块名必须为字符串常量,这意味着下述代码在 ESM 方案下是非法的:
if(process.env.NODE_ENV === 'development'){
import bar from 'bar';
export const foo = 'foo';
}
所以,ESM 下模块之间的依赖关系是高度确定的,与运行状态无关,编译工具只需要对 ESM 模块做静态分析,就可以从代码字面量中推断出哪些模块值未曾被其它模块使用,这是实现 Tree Shaking 技术的必要条件。
对于下述代码:
// index.js
import {bar} from './bar';
console.log(bar);
// bar.js
export const bar = 'bar';
export const foo = 'foo';
示例中,bar.js
模块导出了 bar
、foo
,但只有 bar
导出值被其它模块使用,经过 Tree Shaking 处理后,foo
变量会被视作无用代码删除。
Webpack 中,Tree-shaking 的实现一是先「标记」出模块导出值中哪些没有被用过,二是使用 Terser 删掉这些没被用到的导出语句。标记过程大致可划分为三个步骤:
标记功能需要配置
optimization.usedExports = true
开启
也就是说,标记的效果就是删除没有被其它模块使用的导出语句,比如:
示例中,bar.js
模块(左二)导出了两个变量:bar
与 foo
,其中 foo
没有被其它模块用到,所以经过标记后,构建产物(右一)中 foo
变量对应的导出语句就被删除了。作为对比,如果没有启动标记功能(optimization.usedExports = false
时),则变量无论有没有被用到都会保留导出语句,如上图右二的产物代码所示。
注意,这个时候 foo
变量对应的代码 const foo='foo'
都还保留完整,这是因为标记功能只会影响到模块的导出语句,真正执行“「Shaking」”操作的是 Terser 插件。例如在上例中 foo
变量经过标记后,已经变成一段 Dead Code —— 不可能被执行到的代码,这个时候只需要用 Terser 提供的 DCE 功能就可以删除这一段定义语句,以此实现完整的 Tree Shaking 效果。
接下来我会展开标记过程的源码,详细讲解 Webpack 5 中 Tree Shaking 的实现过程,对源码不感兴趣的同学可以直接跳到下一章。
首先,Webpack 需要弄清楚每个模块分别有什么导出值,这一过程发生在 make 阶段,大体流程:
关于 Make 阶段的更多说明,请参考前文 [[万字总结] 一文吃透 Webpack 核心原理] 。
module
对象的 dependencies
集合,转换规则:HarmonyExportSpecifierDependency
对象default
导出转换为 HarmonyExportExpressionDependency
对象例如对于下面的模块:
export const bar = 'bar';
export const foo = 'foo';
export default 'foo-bar'
对应的dependencies
值为:
2 . 所有模块都编译完毕后,触发 compilation.hooks.finishModules
钩子,开始执行 FlagDependencyExportsPlugin
插件回调
3 . FlagDependencyExportsPlugin
插件从 entry 开始读取 ModuleGraph 中存储的模块信息,遍历所有 module
对象
4 . 遍历 module
对象的 dependencies
数组,找到所有 HarmonyExportXXXDependency
类型的依赖对象,将其转换为 ExportInfo
对象并记录到 ModuleGraph 体系中
经过 FlagDependencyExportsPlugin
插件处理后,所有 ESM 风格的 export 语句都会记录在 ModuleGraph 体系内,后续操作就可以从 ModuleGraph 中直接读取出模块的导出值。
参考资料:1. [[万字总结] 一文吃透 Webpack 核心原理] 2. [有点难的 webpack 知识点:Dependency Graph 深度解析]
模块导出信息收集完毕后,Webpack 需要标记出各个模块的导出列表中,哪些导出值有被其它模块用到,哪些没有,这一过程发生在 Seal 阶段,主流程:
compilation.hooks.optimizeDependencies
钩子,开始执行 FlagDependencyUsagePlugin
插件逻辑FlagDependencyUsagePlugin
插件中,从 entry 开始逐步遍历 ModuleGraph 存储的所有 module
对象module
对象对应的 exportInfo
数组exportInfo
对象执行 compilation.getDependencyReferencedExports
方法,确定其对应的 dependency
对象有否被其它模块使用exportInfo.setUsedConditionally
方法将其标记为已被使用。exportInfo.setUsedConditionally
内部修改 exportInfo._usedInRuntime
属性,记录该导出被如何使用上面是极度简化过的版本,中间还存在非常多的分支逻辑与复杂的集合操作,我们抓住重点:标记模块导出这一操作集中在 FlagDependencyUsagePlugin
插件中,执行结果最终会记录在模块导出语句对应的 exportInfo._usedInRuntime
字典中。
经过前面的收集与标记步骤后,Webpack 已经在 ModuleGraph 体系中清楚地记录了每个模块都导出了哪些值,每个导出值又没那块模块所使用。接下来,Webpack 会根据导出值的使用情况生成不同的代码,例如:
重点关注 bar.js
文件,同样是导出值,bar
被 index.js
模块使用因此对应生成了 __webpack_require__.d
调用 "bar": ()=>(/* binding */ bar)
,作为对比 foo
则仅仅保留了定义语句,没有在 chunk 中生成对应的 export。
关于 Webpack 产物的内容及
__webpack_require__.d
方法的含义,可参考 [Webpack 原理系列六:彻底理解 Webpack 运行时] 一文。
这一段生成逻辑均由导出语句对应的 HarmonyExportXXXDependency
类实现,大体的流程:
HarmonyExportXXXDependency.Template.apply
方法生成代码apply
方法内,读取 ModuleGraph 中存储的 exportsInfo
信息,判断哪些导出值被使用,哪些未被使用HarmonyExportInitFragment
对象,保存到 initFragments
数组initFragments
数组,生成最终结果基本上,这一步的逻辑就是用前面收集好的 exportsInfo
对象未模块的导出值分别生成导出语句。
经过前面几步操作之后,模块导出列表中未被使用的值都不会定义在 __webpack_exports__
对象中,形成一段不可能被执行的 Dead Code 效果,如上例中的 foo
变量:
在此之后,将由 Terser、UglifyJS 等 DCE 工具“摇”掉这部分无效代码,构成完整的 Tree Shaking 操作。
综上所述,Webpack 中 Tree Shaking 的实现分为如下步骤:
FlagDependencyExportsPlugin
插件中根据模块的 dependencies
列表收集模块导出值,并记录到 ModuleGraph 体系的 exportsInfo
中FlagDependencyUsagePlugin
插件中收集模块的导出值的使用情况,并记录到 exportInfo._usedInRuntime
集合中HarmonyExportXXXDependency.Template.apply
方法中根据导出值的使用情况生成不同的导出语句上述实现原理对背景知识要求较高,建议读者同步配合以下文档食用:
- [[万字总结] 一文吃透 Webpack 核心原理]
- [有点难的 webpack 知识点:Dependency Graph 深度解析]
- [Webpack 原理系列六:彻底理解 Webpack 运行时]
虽然 Webpack 自 2.x 开始就原生支持 Tree Shaking 功能,但受限于 JS 的动态特性与模块的复杂性,直至最新的 5.0 版本依然没有解决许多代码副作用带来的问题,使得优化效果并不如 Tree Shaking 原本设想的那么完美,所以需要使用者有意识地优化代码结构,或使用一些补丁技术帮助 Webpack 更精确地检测无效代码,完成 Tree Shaking 操作。
使用 Webpack 时,需要有意识规避一些不必要的赋值操作,观察下面这段示例代码:
示例中,index.js
模块引用了 bar.js
模块的 foo
并赋值给 f
变量,但后续并没有继续用到 foo
或 f
变量,这种场景下 bar.js
模块导出的 foo
值实际上并没有被使用,理应被删除,但 Webpack 的 Tree Shaking 操作并没有生效,产物中依然保留 foo
导出:
造成这一结果,浅层原因是 Webpack 的 Tree Shaking 逻辑停留在代码静态分析层面,只是浅显地判断:
没有进一步,从语义上分析模块导出值是不是真的被有效使用。
更深层次的原因则是 JavaScript 的赋值语句并不「纯」,视具体场景有可能产生意料之外的副作用,例如:
import { bar, foo } from "./bar";
let count = 0;
const mock = {}
Object.defineProperty(mock, 'f', {
set(v) {
mock._f = v;
count += 1;
}
})
mock.f = foo;
console.log(count);
示例中,对 mock
对象施加的 Object.defineProperty
调用,导致 mock.f = foo
赋值语句对 count
变量产生了副作用,这种场景下即使用复杂的动态语义分析也很难在确保正确副作用的前提下,完美地 Shaking 掉所有无用的代码枝叶。
因此,在使用 Webpack 时开发者需要有意识地规避这些无意义的重复赋值操作。
#pure
标注纯函数调用与赋值语句类似,JavaScript 中的函数调用语句也可能产生副作用,因此默认情况下 Webpack 并不会对函数调用做 Tree Shaking 操作。不过,开发者可以在调用语句前添加 /*#__PURE__*/
备注,明确告诉 Webpack 该次函数调用并不会对上下文环境产生副作用,例如:
示例中,foo('be retained')
调用没有带上 /*#__PURE__*/
备注,代码被保留;作为对比,foo('be removed')
带上 Pure 声明后则被 Tree Shaking 删除。
Babel 是一个非常流行的 JavaScript 代码转换器,它能够将高版本的 JS 代码等价转译为兼容性更佳的低版本代码,使得前端开发者能够使用最新的语言特性开发出兼容旧版本浏览器的代码。
但 Babel 提供的部分功能特性会致使 Tree Shaking 功能失效,例如 Babel 可以将 import/export
风格的 ESM 语句等价转译为 CommonJS 风格的模块化语句,但该功能却导致 Webpack 无法对转译后的模块导入导出内容做静态分析,示例:
示例使用 babel-loader
处理 *.js
文件,并设置 Babel 配置项 modules = 'commonjs'
,将模块化方案从 ESM 转译到 CommonJS,导致转译代码(右图上一)没有正确标记出未被使用的导出值 foo
。作为对比,右图 2 为 modules = false
时打包的结果,此时 foo
变量被正确标记为 Dead Code。
所以,在 Webpack 中使用 babel-loader
时,建议将 babel-preset-env
的 moduels
配置项设置为 false
,关闭模块导入导出语句的转译。
Tree Shaking 逻辑作用在 ESM 的 export
语句上,因此对于下面这种导出场景:
export default {
bar: 'bar',
foo: 'foo'
}
即使实际上只用到 default
导出值的其中一个属性,整个 default
对象依然会被完整保留。所以实际开发中,应该尽量保持导出值颗粒度和原子性,上例代码的优化版本:
const bar = 'bar'
const foo = 'foo'
export {
bar,
foo
}
如果可以的话,应尽量使用支持 Tree Shaking 的 npm 包,例如:
lodash-es
替代 lodash
,或者使用 babel-plugin-lodash
实现类似效果不过,并不是所有 npm 包都存在 Tree Shaking 的空间,诸如 React、Vue2 一类的框架原本已经对生产版本做了足够极致的优化,此时业务代码需要整个代码包提供的完整功能,基本上不太需要进行 Tree Shaking。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/McigcfZyIuuA-vfOu3F7VQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。