本篇是 Rspack 源码解析系列第六篇。第四、五篇为了连续说明 ChunkGraph 和代码生成,已经先走到了流水线中段。本篇回到
BuildModuleGraphPhasePass之后,补齐FinishModulesPhasePass、SealPass与OptimizeDependenciesPass:模块图何时可以被冻结,Tree Shaking 又从哪里开始。
前言:图建完了,为什么还不能马上切 Chunk
BuildModuleGraphPhasePass 完成时,Rspack 已经有 Module、Dependency 和 Connection,但图还处于“可继续补充信息”的状态。后续优化至少还要知道三件事:
源码依赖图
-> 模块究竟提供哪些 export
-> 某条 connection 在当前条件下是否有效
-> 模块能否安全地因为没有副作用而被跳过
因此真实顺序是:
BuildModuleGraphPhasePass
-> FinishModulesPhasePass
-> SealPass
-> OptimizeDependenciesPass
-> BuildChunkGraphPass
这三个 Pass 的职责不是再解析文件,而是把“构建时发现的图”转换为“可以用于优化和分包的图”。
FinishModules:冻结优化所需的模块信息
入口在 crates/rspack_core/src/compilation/finish_modules/mod.rs:
pub async fn finish_modules_pass(compilation: &mut Compilation) -> Result<()> {
let mut diagnostics = compilation.dependencies_diagnostics_artifact.steal();
let mut async_modules = compilation.async_modules_artifact.steal();
let mut exports_info = compilation.exports_info_artifact.steal();
let diagnostics = compilation
.finish_modules_inner(&mut diagnostics, &mut async_modules, &mut exports_info)
.await;
compilation.dependencies_diagnostics_artifact = diagnostics.into();
compilation.async_modules_artifact = async_modules.into();
compilation.exports_info_artifact = exports_info.into();
compilation.extend_diagnostics(diagnostics?);
Ok(())
}
finish_modules_inner() 会调用 compilation_hooks.finish_modules。提供导出信息、推断 async module 等工作由内置插件通过 Hook 插入;核心 Pass 负责确定边界、转交 artifact 和收集诊断。
ModuleGraph
+ ExportsInfoArtifact
+ AsyncModulesArtifact
-> finish_modules hook
-> 产出稳定的 export / async 信息
-> 收集 dependency diagnostics
steal() 是什么 Rust 设计
这里连续出现的 StealCell/steal() 值得注意。插件调用需要同时拿到 &Compilation 与可变 artifact;若 artifact 永远挂在 Compilation 内,会很容易撞上 Rust 的借用规则。实现采取短暂“取出 - 修改 - 放回”的模式:
let mut artifact = compilation.exports_info_artifact.steal();
hook.call(compilation, &mut artifact).await?;
compilation.exports_info_artifact = artifact.into();
它把可变状态限定在一个局部变量中,既避免长期可变借用,也使异步 Hook 前后的所有权转换非常明确。这里不是绕开借用检查,而是把状态迁移表达出来。
Seal:从 Make 转入优化阶段的边界
SealPass 的实现很短:
async fn run_pass(&self, compilation: &mut Compilation) -> Result<()> {
let mut diagnostics = vec![];
compilation
.plugin_driver
.clone()
.compilation_hooks
.seal
.call(compilation, &mut diagnostics)
.await?;
compilation.extend_diagnostics(diagnostics);
Ok(())
}
它看似没有做业务逻辑,但意义很清楚:核心不把“图构建结束后该做什么”硬编码在一个巨型函数里。seal 是插件开始为优化、分包、生成做准备的同步点。
可以把它理解为编译事务的阶段切换:
Make:允许创建模块、连接和依赖
|
v
Seal:宣布模块图的基本结构已确定
|
v
Optimize:只在既有图上分析、删除、重组
“冻结”不表示 Rust 对象变成不可变;而是编译器约定后续 Pass 不再随意创建会改变图语义的新模块连接。这个约定让各插件可以放心依赖前一阶段的结果。
OptimizeDependencies:从依赖边开始做优化
入口实现:
while matches!(
compilation.plugin_driver.clone().compilation_hooks
.optimize_dependencies
.call(compilation, &mut side_effects, &mut module_graph, &mut exports, &mut diagnostics)
.await?,
Some(true)
) {}
这里的 while Some(true) 并非普通循环。它对应 webpack 兼容的 bail/repeat Hook 语义:某个优化插件认为“这轮修改了依赖关系,还需要重新跑一轮”时返回 Some(true);没有插件要求重跑后循环结束。
典型的输入与输出是:
| 输入 | 优化动作 | 输出 |
|---|---|---|
import { used } from './a' | 标记实际使用的 export | 未使用 export 可在后续被裁剪 |
| 标记为无副作用的模块 | 判断引用是否只为副作用 | 无用模块可跳过 |
| 条件依赖 | 判断 connection 是否 active | 不参与当前构建的边失效 |
注意这里不是立即删除所有模块。依赖优化首先调整“哪些连接和导出是有效的”,模块和 Chunk 的最终删除、合并由后面不同粒度的优化 Pass 完成。
用一个例子串起来
// index.ts
import { used } from './utils';
import './polyfill';
console.log(used());
// utils.ts
export const used = () => 1;
export const unused = () => 2;
完成模块构建后,utils.ts 的两个导出都存在。Finish 阶段建立它“提供了 used、unused”的事实;依赖优化阶段发现入口只请求 used。后面的 code generation 才能据此不生成 unused 的最终内容。
解析结果:utils 提供 used、unused
-> Finish:导出信息完整可查询
-> OptimizeDependencies:used 被使用,unused 未使用
-> 后续优化 / 生成:裁剪 unused
polyfill 则不同:即使它没有导出被使用,只要带有副作用,就不能仅凭“没有 import binding”移除。这正是 side-effects 分析存在的原因。
这一篇应该带走什么
FinishModules让模块图具备可用于优化的 export、async 和诊断信息;Seal是 Make 到优化阶段的架构边界,具体策略由插件填充;OptimizeDependencies优化的是依赖边和使用关系,而不是简单删文件;steal -> hook -> into是 Rspack 在异步插件调用中管理可变 artifact 的重要 Rust 模式;Some(true)重跑协议让多个优化插件可以迭代到稳定状态。
写在最后
现在 ModuleGraph 已有可用的导出与副作用信息,第四篇的 BuildChunkGraphPass 才能据此决定哪些模块进入 Chunk。ChunkGraph 建好之后,后面还有一组更高层的优化:模块、Chunk、树和 Chunk 内模块会分别在不同粒度上调整。下一篇就沿这四个 Pass 继续追踪。