前言
在前4篇文章中,我们大致已经对Rollup的构建及产物生成逻辑略知一二了,在这个过程中,我们有意的跳过了一些复杂但有用的知识点,比如TreeShaking就是一个有用的知识点。
在这一篇文章中,我们就从Rollup的源码角度来分析一下它是如何完成TreeShaking这一伟大的优化操作的。
这篇文章和前面的第二篇文章有一些关联,在阅读这篇文章之前,已经阅读过之前的文章。
TreeShaking的概念与优势
现实生活中有一个生动形象的例子可以与之对应,当到了秋天,树叶黄了,我们摇一摇树干,就会被树上的枯叶摇掉,落红不是无情物,化作春泥更护花,呵呵。
树摇优化即Tree Shaking 是一种在打包工具(如 Rollup、Webpack、esbuild、Parcel 等)中常用的优化技术,它通过分析代码的依赖关系和可达性,将未被实际使用(导出但未被引用)的代码 "摇掉",从而减小最终的打包结果大小。
简单来说,TreeShaking会在构建过程中检查你的模块中到底有哪些函数、类、变量从未真正使用,然后在最终产物中将这些无关代码删除掉。
核心前提
-
必须是基于
ES Module
风格的代码,因为ESM是静态可分析的,也就是在编译时能明确知道哪个模块导出什么,哪些导出在其它模块被使用。 -
严格模式与无副作用函数,当函数或者模块执行会产生副作用(如修改全局变量、console 输出等),Tree Shaking 会变得困难和不可靠。打包工具往往要求开发者申明某些包或模块是无副作用的,以便安全地移除未使用的导出。对于一些可能有副作用的内容,可以在项目的package.json文件中的
sidEffects
中指定,这样可以告诉打包工作,在这些目录下面的文件都是不能被移除的。 -
有些打包工具需要要求在生产模式下。
意义与好处
- 减小代码体积 :
通过移除未使用的代码,最终生成的 bundle 文件体积更小,加载更快,提高前端性能与用户体验。 - 提升加载速度 :
更小的代码包意味着用户在加载页面时消耗的时间和流量更少,有助于减少白屏时间及加快首屏渲染。 - 优化资源利用 :
减少无用代码也可让浏览器更快地解析、执行代码,进而降低 CPU 和内存占用。 - 有利于代码维护 :
当开发者察觉到某些函数、模块完全未被使用却依然保留在代码中,这会促使他们更好地维护代码库,清理冗余依赖,保持代码整洁。这也是可以防止我们写出屎山代码的一种微不足道的方式。
Rollup如何进行TreeShaking
之前的项目准备的有点儿复杂,在这一章节,我们就用2个简单的文件来尝试即可。
主入口文件:
index.js
import { demo1, demo2 } from "./demo";
function demo() {
console.log("demo");
demo1();
if (process.env.NODE_ENV !== "production") {
demo2();
}
}
demo();
用来测试TreeShaking的文件:
js
export function demo1() {
console.log("demo1");
}
export function demo2() {
console.log("demo2");
}
在之前的文章中,我们已经提到过,Module类是Rollup中处理文件的核心类,它身上绑定着文件的源码,解析得到的AST
,以及与之相关的模块引用关系。
引用标记
我们暂时先回到Module的setSource
方法: 在准备做AST解析的时候,Rollup初始化了一个辅助上下文,这个上下文关联了很多操作,在解析AST的时候届时文件的关联操作就可以通过这个上下文进行。
到这会儿,暂时还没有看到最终生成代码引用关系的建立,这个位置只确定了文件的依赖关系。
到这个位置的时候,就确定了文件内容代码层面上的依赖关系了。 我们先看看这个includeStatements
函数: 看这个语句的意思有点儿接近我们想要的答案了,如果直接配置了不进行TreeShaking,那么就直接把所有内容打包到bundle,如果没有的话,再考虑处理代码层面的引用关系了。
因为接下来的内容都是AST的操作,操作AST的内容又臭又长,也不好进行直观的查看,我们通过调试查看堆栈的形式,反向推导。
已知上面我们给出来的代码,demo2
函数是要被摇掉的,那么,我们先看一下renderChunk
之后,这个Module里面剩下的数据情况是什么样的。
那么,我们需要看一下,这个includedImports
是在什么位置添加的就好了。
我们直接在源码里面搜索: 打一个断点,查看堆栈信息。 恭喜,我们可以通过堆栈信息找到是在这个位置处理的引用标记,这是在用户不关闭TreeShaking的能力的场景下走到的这个分支。 接下来,就回到冗余无味的AST处理引用的标记了。 这个AST的来源,即是之前我们读取文件内容时,解析到的AST。
关于AST的处理引用的问题,如果全都写出来,篇幅太长了,在本文中这部分知识点,我就做这么多解析,如果读者有任何问题,可以和我交流。
内容输出消除
在之前的小节,我们已经分析到了,Rollup通过对AST的分析,确定文件之间引用的内容,接下来,我们需要看一下Rollup在做内容输出的时候,是如何做到删除未引用的内容仅仅输出有用的内容的逻辑。
在上一篇文章中,我们在聊Rollup的输出逻辑的时候已经讲过了Module上有一个render方法,主要的功能是把当前文件的内容输出。
这个调用关系是上面几个图的说明: Bundle.renderChunks
->renderChunks
->Chunk.render
->Chunk.renderModules
->Module.render
在Module里面,调用AST的render方法,将AST重新转换成目标代码。 这个方法是一个基类的方法,AST处理逻辑中,有很多种子类处理语句,每种语句均实现render方法,负责把对应的AST输出成目标代码,所以我们可以搜索到很多实现。
所以,再看这个位置跳转过去的ast的处理逻辑就没有多大的意义了。 不过,还是给大家看一看,哈哈。 这个处理是处理Program类型的AST,AST的Root节点正是这个类型的节点。 如果当前抽象语法树节点没有被标记引入,则认为当前的节点是可以被丢弃的。 我们还是通过打断点的方式来看一下,我们之前写的demo里面demo2
函数是如何被摇掉的。 这儿AST的仍然存在不直观的问题,我们直接把那个magicString处理前后打印出来,就知道了。 到这个位置,我们就已经把Rollup如何进行TreeShaking的原理讲清楚了。
总结
最后,给大家总结一下Rollup进行TreeShaking的总体流程。
首先在构建阶段,Rollup在正常解析文件,初始化一个Module类用来保存文件的内容信息,Rollup会将将解析到文件内容转成AST存储在Module中。
当所有的文件都已经解析完成之后,如果用户配置的是不进行TreeShaking的话,那么所有的那内容都讲打包到最后的bundle中。
如用户配置需要TreeShaking的话,Rollup会借助AST对文件引用的内容进行分析,它将会分析出哪些抽象语法树节点是需要的,哪些节点是不需要的,完成初步的标记。
然后到了生成阶段,Rollup先根据依赖或者自定义的Chunk划分依据,划分出不同的Chunk,然后Chunk调用自身所关联的所有Module,Module根据自身存储的AST信息,可以把AST根据用户的需求重新转换成目标代码。
而这个过程中,因为我们在构建阶段已经对AST进行了标记,我们已经清楚的知道哪些抽象语法树节点是需要的,哪些节点是不需要的,然后在生成目标代码的过程中,选择性的忽略一些内容,从而实现将不需要的内容消掉,减少最终的Bundle体积。
相信很多读者学习这些知识点,更多的是为了面试做准备吧,我也总结一个简单的步骤帮助大家记忆:
源代码->AST->标记与分析->AST输出(跳过不需要节点)->目标代码
以上就是我通过阅读Rollup源代码得出其进行TreeShaking的底层原理,分析过程中如果存在纰漏或错误还请大家谅解,如果读者有任何质疑或者困惑可以尝试和我联系,你们的建议和意见是我进步的不竭动力。