Rspack 源码解析(七):Module、Chunk 与加载树优化

本篇是 Rspack 源码解析系列第七篇。ChunkGraph 建好后,Rspack 并不会立刻生成代码,而是依次执行 OptimizeModulesPassOptimizeChunksPassOptimizeTreePassOptimizeChunkModulesPass。它们回答的是:已知依赖图和分包图后,怎样让最终体积和加载关系更合理。

前言:优化不是一个 Pass

源码中的顺序是:

text 复制代码
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 的共同形态

以模块优化为例:

rust 复制代码
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?;

这段代码包含两层设计:

  1. Hook 的插件顺序由 PluginDriver 统一管理,Pass 不依赖某个具体插件;
  2. Some(true) 表示本轮图被改变,需要再次运行同一优化阶段直到稳定。

OptimizeChunksPass 也使用重复执行;OptimizeTreePassOptimizeChunkModulesPass 则是一次调用。是否允许迭代由该阶段的语义决定,而不是所有 Hook 都无条件循环。

模块级:先决定哪些源码逻辑值得保留

模块优化消费第六篇得到的 exports 和 side effects 信息。它通常会驱动:

text 复制代码
used exports
  -> 标记不可达导出
  -> 移除无副作用且不可达的模块
  -> 条件满足时做模块拼接(scope hoisting)

模块拼接并不是把文本字符串直接相连。Rspack 中有 ConcatenatedModule 一类模块抽象,它仍然以 Module 身份参与后续 CodeGeneration。这样优化后的结果不会破坏 Module::code_generation() 这一统一接口。

从 Rust 角度看,这说明 trait object 的价值:后续阶段只依赖 dyn Module 的行为,不需要为 NormalModuleConcatenatedModule 分别写整条流水线。

Chunk 级:splitChunks 为什么在这里生效

第四篇的 BuildChunkGraph 只根据入口、同步依赖和 import() 建立初始分组。公共模块的提取、Chunk 合并等策略属于 OptimizeChunksPass

text 复制代码
entry-a -> shared
entry-b -> shared

初始:shared 同时在 a、b 的 Chunk 中
  -> Chunk 优化插件识别复用与阈值
  -> 创建 common Chunk 或调整归属
  -> a、b 改为依赖 common

是否抽取不是"共享就一定抽"。还要考虑 minSizeminChunks、请求数量和 cache group。将它放在 ChunkGraph 已构建之后,插件可以看到完整的模块归属和 ChunkGroup 父子关系。

Tree 级:从单个 Chunk 到加载路径

一个模块位于多个 Chunk 时,单看某个 Chunk 不足以判断它是否可用。OptimizeTreePass 处理的是入口到异步 ChunkGroup 的整体结构:

text 复制代码
Entrypoint(main)
  -> Chunk(main)
  -> ChunkGroup(lazy)
      -> Chunk(lazy)

这一级优化需要考虑 parent、child、runtime 和可用模块集合。例如把模块提升到父 Chunk 可能减少异步请求,但也会扩大初始包;将其保留在异步 Chunk 则相反。由树级插件作决策,能避免各个 Chunk 局部修改造成加载关系不一致。

Chunk 内模块级:最后整理具体归属

OptimizeChunkModulesPass 的核心调用是:

rust 复制代码
compilation.plugin_driver.clone().compilation_hooks
  .optimize_chunk_modules
  .call(compilation)
  .await?;

它位于 tree 优化之后,因为此时 Chunk 结构已基本稳定。此阶段适合完成"某个模块在某个 Chunk 中是否仍需要存在"的最后调整。Pass 前后还调用 cache Hook:

rust 复制代码
async fn before_pass(&self, compilation: &mut Compilation, cache: &mut dyn Cache) {
  cache.before_optimize_chunk_modules(compilation).await;
}

这使缓存实现可以按阶段保存或恢复结果,而不是把缓存逻辑散落到每个优化插件。

一张流程图

text 复制代码
稳定 ModuleGraph + ChunkGraph
       |
       v
OptimizeModules      以 Module 为单位减少/合并代码
       |
       v
OptimizeChunks       以 Chunk 为单位执行 splitChunks 等策略
       |
       v
OptimizeTree         校验并优化入口到异步 Chunk 的加载树
       |
       v
OptimizeChunkModules 最终调整模块和具体 Chunk 的归属
       |
       v
准备给模块、Chunk 和 runtime 分配可执行身份

这一篇应该带走什么

  1. 优化阶段按 Module、Chunk、Tree、Chunk-Module 四个粒度分层;
  2. splitChunks 的输入是已完成的 ChunkGraph,而不是原始文件列表;
  3. 反复执行的 Hook 使用 Some(true) 作为明确的"尚未稳定"信号;
  4. Core 只调度阶段,内置或用户插件决定具体优化算法;
  5. 优化后仍然保留统一的 Module 抽象,后续代码生成不必知道某模块是否被拼接过。

写在最后

当图结构不再变化,Rspack 就可以开始给 Module、Chunk 和 runtime 分配 ID。这个阶段表面上只是命名,实际上直接影响模块加载、缓存命中和生成代码的稳定性。下一篇进入三类 ID 的分配过程。

相关推荐
OpenTiny社区1 小时前
从开发到运行:OpenTiny NEXT 如何构建 Agent 时代的 Web 应用?
前端·github·ai编程
愿得一猪蹄1 小时前
前端性能优化,Core Web Vitals 三个指标到底怎么改
前端
xy34531 小时前
Axure 9.0 中继器创建与设置步骤
前端·ui·html·axure·原型·产品设计
FEF前端团队1 小时前
01# Vibe Coding工作流设计思维:前端开发者的协作框架
前端·cursor·vibecoding
计算机魔术师1 小时前
OpenAI 发布模型错位报告框架,披露未发布模型自行修改自身指令案例
前端
特创数字科技2 小时前
公文Word一键批量排版工具|批量统一公文标准格式,办公自动化桌面小工具
前端·python
流水白开2 小时前
JavaScript的原型链、类与类的继承
前端·javascript
PedroQue992 小时前
修复 .uvue 页面路径残留问题
前端·vite