Rspack 源码解析(六):模块图收尾与依赖优化

5 阅读4分钟

本篇是 Rspack 源码解析系列第六篇。第四、五篇为了连续说明 ChunkGraph 和代码生成,已经先走到了流水线中段。本篇回到 BuildModuleGraphPhasePass 之后,补齐 FinishModulesPhasePassSealPassOptimizeDependenciesPass:模块图何时可以被冻结,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 阶段建立它“提供了 usedunused”的事实;依赖优化阶段发现入口只请求 used。后面的 code generation 才能据此不生成 unused 的最终内容。

解析结果:utils 提供 used、unused
       -> Finish:导出信息完整可查询
       -> OptimizeDependencies:used 被使用,unused 未使用
       -> 后续优化 / 生成:裁剪 unused

polyfill 则不同:即使它没有导出被使用,只要带有副作用,就不能仅凭“没有 import binding”移除。这正是 side-effects 分析存在的原因。

这一篇应该带走什么

  1. FinishModules 让模块图具备可用于优化的 export、async 和诊断信息;
  2. Seal 是 Make 到优化阶段的架构边界,具体策略由插件填充;
  3. OptimizeDependencies 优化的是依赖边和使用关系,而不是简单删文件;
  4. steal -> hook -> into 是 Rspack 在异步插件调用中管理可变 artifact 的重要 Rust 模式;
  5. Some(true) 重跑协议让多个优化插件可以迭代到稳定状态。

写在最后

现在 ModuleGraph 已有可用的导出与副作用信息,第四篇的 BuildChunkGraphPass 才能据此决定哪些模块进入 Chunk。ChunkGraph 建好之后,后面还有一组更高层的优化:模块、Chunk、树和 Chunk 内模块会分别在不同粒度上调整。下一篇就沿这四个 Pass 继续追踪。