本篇是 Rspack 源码解析系列第七篇。ChunkGraph 建好后,Rspack 并不会立刻生成代码,而是依次执行
OptimizeModulesPass、OptimizeChunksPass、OptimizeTreePass和OptimizeChunkModulesPass。它们回答的是:已知依赖图和分包图后,怎样让最终体积和加载关系更合理。
前言:优化不是一个 Pass
源码中的顺序是:
BuildChunkGraphPass
-> OptimizeModulesPass
-> OptimizeChunksPass
-> OptimizeTreePass
-> OptimizeChunkModulesPass
四个 Pass 看起来都只是调用 Hook,正说明 Rspack Core 的职责是定义稳定的阶段和数据边界,具体算法由 rspack_plugin_* 中的插件注册。按对象粒度拆开,避免一个“optimize”同时修改所有数据结构:
| Pass | 主要对象 | 关注点 |
|---|---|---|
OptimizeModulesPass | ModuleGraph | 模块是否可连接、拼接或移除 |
OptimizeChunksPass | Chunk / ChunkGroup | Chunk 如何拆分、合并、命名 |
OptimizeTreePass | ChunkGraph 的整棵加载树 | 跨 Chunk 的可达性与运行时关系 |
OptimizeChunkModulesPass | 一个 Chunk 内的模块集合 | 模块在具体 Chunk 内的布局 |
四个 Pass 的共同形态
以模块优化为例:
while matches!(
compilation.plugin_driver.clone().compilation_hooks
.optimize_modules
.call(compilation, &mut diagnostics)
.await?,
Some(true)
) {}
compilation.plugin_driver.clone().compilation_hooks
.after_optimize_modules
.call(compilation)
.await?;
这段代码包含两层设计:
- Hook 的插件顺序由
PluginDriver统一管理,Pass 不依赖某个具体插件; Some(true)表示本轮图被改变,需要再次运行同一优化阶段直到稳定。
OptimizeChunksPass 也使用重复执行;OptimizeTreePass、OptimizeChunkModulesPass 则是一次调用。是否允许迭代由该阶段的语义决定,而不是所有 Hook 都无条件循环。
模块级:先决定哪些源码逻辑值得保留
模块优化消费第六篇得到的 exports 和 side effects 信息。它通常会驱动:
used exports
-> 标记不可达导出
-> 移除无副作用且不可达的模块
-> 条件满足时做模块拼接(scope hoisting)
模块拼接并不是把文本字符串直接相连。Rspack 中有 ConcatenatedModule 一类模块抽象,它仍然以 Module 身份参与后续 CodeGeneration。这样优化后的结果不会破坏 Module::code_generation() 这一统一接口。
从 Rust 角度看,这说明 trait object 的价值:后续阶段只依赖 dyn Module 的行为,不需要为 NormalModule、ConcatenatedModule 分别写整条流水线。
Chunk 级:splitChunks 为什么在这里生效
第四篇的 BuildChunkGraph 只根据入口、同步依赖和 import() 建立初始分组。公共模块的提取、Chunk 合并等策略属于 OptimizeChunksPass:
entry-a -> shared
entry-b -> shared
初始:shared 同时在 a、b 的 Chunk 中
-> Chunk 优化插件识别复用与阈值
-> 创建 common Chunk 或调整归属
-> a、b 改为依赖 common
是否抽取不是“共享就一定抽”。还要考虑 minSize、minChunks、请求数量和 cache group。将它放在 ChunkGraph 已构建之后,插件可以看到完整的模块归属和 ChunkGroup 父子关系。
Tree 级:从单个 Chunk 到加载路径
一个模块位于多个 Chunk 时,单看某个 Chunk 不足以判断它是否可用。OptimizeTreePass 处理的是入口到异步 ChunkGroup 的整体结构:
Entrypoint(main)
-> Chunk(main)
-> ChunkGroup(lazy)
-> Chunk(lazy)
这一级优化需要考虑 parent、child、runtime 和可用模块集合。例如把模块提升到父 Chunk 可能减少异步请求,但也会扩大初始包;将其保留在异步 Chunk 则相反。由树级插件作决策,能避免各个 Chunk 局部修改造成加载关系不一致。
Chunk 内模块级:最后整理具体归属
OptimizeChunkModulesPass 的核心调用是:
compilation.plugin_driver.clone().compilation_hooks
.optimize_chunk_modules
.call(compilation)
.await?;
它位于 tree 优化之后,因为此时 Chunk 结构已基本稳定。此阶段适合完成“某个模块在某个 Chunk 中是否仍需要存在”的最后调整。Pass 前后还调用 cache Hook:
async fn before_pass(&self, compilation: &mut Compilation, cache: &mut dyn Cache) {
cache.before_optimize_chunk_modules(compilation).await;
}
这使缓存实现可以按阶段保存或恢复结果,而不是把缓存逻辑散落到每个优化插件。
一张流程图
稳定 ModuleGraph + ChunkGraph
|
v
OptimizeModules 以 Module 为单位减少/合并代码
|
v
OptimizeChunks 以 Chunk 为单位执行 splitChunks 等策略
|
v
OptimizeTree 校验并优化入口到异步 Chunk 的加载树
|
v
OptimizeChunkModules 最终调整模块和具体 Chunk 的归属
|
v
准备给模块、Chunk 和 runtime 分配可执行身份
这一篇应该带走什么
- 优化阶段按 Module、Chunk、Tree、Chunk-Module 四个粒度分层;
splitChunks的输入是已完成的 ChunkGraph,而不是原始文件列表;- 反复执行的 Hook 使用
Some(true)作为明确的“尚未稳定”信号; - Core 只调度阶段,内置或用户插件决定具体优化算法;
- 优化后仍然保留统一的 Module 抽象,后续代码生成不必知道某模块是否被拼接过。
写在最后
当图结构不再变化,Rspack 就可以开始给 Module、Chunk 和 runtime 分配 ID。这个阶段表面上只是命名,实际上直接影响模块加载、缓存命中和生成代码的稳定性。下一篇进入三类 ID 的分配过程。