本篇是 Rspack 源码解析系列第五篇。上一篇我们从 ModuleGraph 得到了 ChunkGraph,已经知道
index.ts、math.ts、lazy.ts各自会被放入哪些 Chunk。但“放进 Chunk”不代表它们已经是浏览器可以执行的源码。本篇进入CodeGenerationPass,看 Rspack 怎样让每个模块针对不同 runtime 生成代码、运行时依赖和 source map 等结果。
前言
先把几个阶段的输出区分开:
Build Module Graph
输出:ModuleGraph
解决:谁依赖谁
Build Chunk Graph
输出:ChunkGraph
解决:谁进入哪个 Chunk,异步 Chunk 如何连接
Code Generation
输出:CodeGenerationResults
解决:每个 Module 在某个 runtime 下具体生成什么代码
Create Chunk Assets
输出:Asset(main.js、lazy.js 等)
解决:如何把一个 Chunk 中的多个模块代码拼成最终文件
本篇只聚焦第三层。CodeGenerationPass 并不直接写出 dist/main.js,而是先为模块生成“可被渲染的代码片段和元数据”。真正将这些片段拼成 asset 的工作在后面的 CreateChunkAssetsPass。
CodeGenerationPass 在流水线中的位置
run_passes.rs 中,代码生成位于一系列优化和标识分配之后:
BuildChunkGraphPass
-> OptimizeModulesPass
-> OptimizeChunksPass
-> OptimizeTreePass
-> OptimizeChunkModulesPass
-> ModuleIdsPass
-> ChunkIdsPass
-> AssignRuntimeIdsPass
-> OptimizeCodeGenerationPass
-> CreateModuleHashesPass
-> CodeGenerationPass
-> RuntimeRequirementsPass
-> CreateHashPass
-> CreateModuleAssetsPass
-> CreateChunkAssetsPass
这个顺序不是偶然的。
- 代码生成需要知道模块在哪些 Chunk/runtime 中执行;
- 模块 ID、Chunk ID 会直接进入生成代码;
- Tree Shaking、模块拼接等优化必须先确定,否则会为最终不需要的模块生成无用代码;
- 模块 hash 在代码生成前创建,用于结果缓存和内容 hash 计算。
换句话说,CodeGenerationPass 是“图优化结果”到“源代码产物”的边界。
先建立四个概念
| 概念 | 可以理解成 | 作用 |
|---|---|---|
Module::code_generation() | 各类模块自己的代码生成方法 | NormalModule、ExternalModule、ContextModule 实现不同逻辑 |
ModuleCodeGenerationContext | 本次生成代码可读取的编译上下文 | 提供 Compilation、runtime、模板和模块拼接 scope |
CodeGenerationResult | 单个模块的生成结果 | 保存不同 SourceType 的 source、runtime requirements、hash 等 |
CodeGenerationResults | 所有模块、所有 runtime 的结果仓库 | 供后续渲染 Chunk 使用 |
这里的关键点是:同一个 Module 可能在多个 runtime 下有不同生成结果。
例如 index.ts 被两个不同 entry 引用,或某个模块在不同 runtime 中有不同的可用导出、模块 ID 或 runtime requirement。Rspack 不能只用 ModuleIdentifier -> Source 的简单映射,而要按 runtime 保存结果。
Pass 入口:增量构建先缩小范围
入口位于 crates/rspack_core/src/compilation/code_generation/mod.rs:
// code_generation/mod.rs(简化)
async fn code_generation_pass_impl(compilation: &mut Compilation) -> Result<()> {
let code_generation_modules = if let Some(mutations) = compilation
.incremental
.mutations_read(IncrementalPasses::MODULES_CODEGEN)
&& !compilation.code_generation_results.is_empty()
{
// 删除已移除模块的旧结果
// 根据 ModuleSetHashes mutation 收集受影响模块
// 只重生成这些模块
modules
} else {
*compilation.code_generation_results = Default::default();
compilation.get_module_graph().modules_keys().copied().collect()
};
compilation.code_generation(code_generation_modules).await?;
compilation.plugin_driver
.compilation_hooks
.after_code_generation
.call(compilation, &mut diagnostics)
.await?;
Ok(())
}
首次构建时,所有模块都会进入 code generation。增量构建时,Rspack 利用前面阶段记录的 Mutation::ModuleSetHashes 缩小范围:模块没有变化、生成 hash 没有失效,就无需重新生成。
这与上一篇的 Cutout + repair 是同一种思路:整个编译器不是“文件一变就重做所有阶段”,而是让每个 Artifact 都有自己的变更边界。
为什么要按 runtime 分组
Compilation::code_generation() 并不会立即遍历所有模块调用生成函数。它先区分有无 code generation dependencies 的模块,再交给 code_generation_modules():
// code_generation/mod.rs(简化)
for runtime in chunk_graph.get_module_runtimes_iter(
module,
&self.build_chunk_graph_artifact.chunk_by_ukey,
) {
let hash = ChunkGraph::get_module_hash(self, module, runtime)?;
if let Some(job) = map.get_mut(hash) {
job.runtimes.push(runtime.clone());
} else {
map.insert(hash.clone(), CodeGenerationJob {
module,
hash,
runtime: runtime.clone(),
runtimes: vec![runtime.clone()],
scope,
});
}
}
这段代码有两层去重。
第一层:模块按其运行时遍历
ChunkGraph::get_module_runtimes_iter() 从 ChunkGraph 中取出模块所在 ChunkGroup 的 runtime。一个模块被多个入口共享时,可能对应多个 runtime。
第二层:生成 hash 相同的 runtime 合并成一个 Job
如果多个 runtime 下该模块的生成 hash 相同,Rspack 只生成一次,再将结果复用到多个 runtime:
module A
runtime X -> hash h1
runtime Y -> hash h1
runtime Z -> hash h2
最终 Job:
Job(A, h1, [X, Y])
Job(A, h2, [Z])
这比“每个 Chunk 都重新生成其中每个模块”节省得多。它也说明代码生成结果的缓存粒度不是单纯的文件,而是“模块在等价运行时上下文中的代码”。
并行执行:先组织 Job,再并发生成
所有 CodeGenerationJob 收集完后,Rspack 使用 rspack_futures::scope 并发执行:
// code_generation/mod.rs(简化)
let results = rspack_futures::scope::<_, _>(|token| {
jobs.into_iter().for_each(|job| {
let s = unsafe { token.used((&self, &module_graph, job)) };
s.spawn(|(this, module_graph, job)| async {
let module = module_graph
.module_by_identifier(&job.module)
.expect("should have module");
module.code_generation(&mut code_generation_context).await
})
})
})
.await;
和第三篇 Build Module Graph 的后台任务设计形成对照:
- 模块图构建时,共享图结构会被串行写入,因此需要把
AddTask等操作放回主任务队列; - 代码生成时,输入的 ModuleGraph、ChunkGraph 和前序优化结果已经稳定,每个 Job 产出独立的
CodeGenerationResult,因此适合并行计算。
这体现了一个重要的并发设计原则:不是所有编译阶段都需要同一种并发模型;能否并行取决于任务是否写同一份共享状态。
ModuleCodeGenerationContext:模块生成时能看到什么
每个 Job 会创建 ModuleCodeGenerationContext:
// code_generation/mod.rs(简化)
let mut runtime_template = this.runtime_template.create_module_code_template();
let mut code_generation_context = ModuleCodeGenerationContext {
compilation: this,
runtime: Some(&job.runtime),
concatenation_scope: job.scope.clone(),
runtime_template: &mut runtime_template,
};
module.code_generation(&mut code_generation_context).await
模块生成代码时不应该自己持有整个编译器的可变状态,而是通过这个上下文读取所需能力:
| 字段 | 用途 |
|---|---|
compilation | 读取 options、ModuleGraph、ChunkGraph、插件钩子等编译信息 |
runtime | 按当前 runtime 判断依赖连接、导出名和运行时代码需求 |
concatenation_scope | 支持 Module Concatenation 等优化后的模块组合 |
runtime_template | 生成 __webpack_require__ 等 runtime 调用模板,并收集 runtime requirements |
这是很典型的 Rust API 设计:将一次操作实际需要的上下文显式封装为参数,而不是让模块对象依赖全局状态。
NormalModule 的代码生成发生了什么
不同模块类型各自实现 Module trait 中的 code_generation():
// crates/rspack_core/src/module.rs(示意)
#[async_trait]
pub trait Module {
async fn code_generation(
&self,
context: &mut ModuleCodeGenerationContext,
) -> Result<CodeGenerationResult>;
}
例如:
NormalModule:对 JavaScript/TypeScript 经 Loader、Parser 处理后的源码进行依赖替换和封装;ExternalModule:生成对外部变量、CommonJS 或 ESM import 的引用代码;ContextModule:生成上下文模块的查找映射;ConcatenatedModule:生成 Scope Hoisting 后拼接的模块代码。
NormalModule::code_generation() 位于 crates/rspack_core/src/normal_module.rs。它不是再去读源文件或执行 Loader;这些工作已经在第三篇的 BuildTask 中完成。此时它消费已经构建好的 source 和 dependencies,并让 dependency template 根据当前 runtime 改写对应位置。
可以把这个阶段粗略理解成:
Loader / Parser 之后的源码
+ 依赖信息
+ 当前 ModuleId / runtime
+ dependency templates
-> 浏览器运行时可调用的模块源码
例如源码:
import { sum } from './math';
console.log(sum(1, 2));
经过模块化代码生成后,不再只是原始 import 文本,而会变成与模块 ID、模块工厂和 runtime loader 协作的代码形态。具体字符串会随 target、output、模块格式和优化配置而变,但原则不变:模块之间的引用从“源码层的请求”变为“运行时能解析的模块引用”。
CodeGenerationResult 包含的不只是 JavaScript
module.code_generation() 返回的不是单一字符串,而是 CodeGenerationResult。从后续渲染角度,它至少要表达:
不同 SourceType 的 Source
+ runtime_requirements
+ data
+ hash
SourceType 允许模块产生不止一种类型的输出,例如 JavaScript、CSS 或 runtime 相关 source。后面的 Chunk 渲染器会按 source type 组合模块结果。
runtime_requirements 同样重要。模块本身生成代码时可能发现需要某个运行时全局能力,例如:
__webpack_require__
模块缓存
异步 Chunk 加载
公开路径
ESM namespace helpers
这些 requirement 先由模块生成阶段收集,再由后面的 RuntimeRequirementsPass 汇总到 Chunk 和 Tree,最后再添加所需 runtime module。这样一个普通模块不用自己直接拼完整 runtime,只需要声明“我用到了什么”。
源码中在生成完成后会合并模板收集到的 requirement,并计算结果 hash:
// code_generation/mod.rs(简化)
module.code_generation(&mut code_generation_context)
.await
.map(|mut codegen_res| {
codegen_res.runtime_requirements.extend(
*runtime_template.runtime_requirements()
);
codegen_res.set_hash(
&options.output.hash_function,
&options.output.hash_digest,
&options.output.hash_salt,
);
codegen_res
})
缓存:模块代码生成结果如何复用
生成代码前,Rspack 会通过 code_generate_cache_artifact.use_cache() 查询缓存:
// code_generation/mod.rs(简化)
let codegen_res = this
.code_generate_cache_artifact
.use_cache(&job, || async {
module.code_generation(&mut code_generation_context).await
})
.await;
缓存键对应 CodeGenerationJob,因此自然包含模块、hash 和 runtime 相关信息。对于未变更模块,缓存命中时就可以避免重复执行 dependency template 和源码转换。
同时,代码生成错误不会让 Rspack 在这里立刻丢弃所有其他模块结果:
let codegen_res = match codegen_res {
Ok(codegen_res) => codegen_res,
Err(err) => {
let mut diagnostic = Diagnostic::from(err);
diagnostic.module_identifier = Some(module);
self.push_diagnostic(diagnostic);
CodeGenerationResult::default()
}
};
这延续了第三篇 Factorize/Build 阶段的设计:尽可能收集完整 diagnostics,最后统一向用户报告,而不是遇到第一个模块失败就停止整个编译流程。
结果如何写回 Compilation
每个并发 Job 完成后,主流程将结果写回 code_generation_results:
// code_generation/mod.rs(简化)
self
.code_generation_results
.insert(module, codegen_res, runtimes);
self.code_generated_modules.insert(module);
可以把这个 Artifact 理解为:
CodeGenerationResults
Module(index.ts)
Runtime(main) -> JS source + requirements + hash
Module(math.ts)
Runtime(main) -> JS source + requirements + hash
Module(lazy.ts)
Runtime(lazy) -> JS source + requirements + hash
后续阶段不会重新调用每个 Module 的 code_generation(),而是从这里读取结果来:
- 聚合 runtime requirements;
- 计算 compilation/chunk hash;
- 渲染模块代码;
- 生成
main.js、异步 Chunk 和 source map 等最终 asset。
用前三篇的示例走一遍
继续使用:
// index.ts
import { sum } from './math';
import('./lazy');
console.log(sum(1, 2));
在上一阶段,ChunkGraph 已经得到:
main Chunk: index.ts, math.ts
lazy Chunk: lazy.ts
现在 CodeGenerationPass 会做:
index.ts @ main runtime
-> 生成模块代码
-> 声明同步引用 math.ts 所需的 runtime helper
-> 声明 import() 所需的异步 Chunk 加载 helper
math.ts @ main runtime
-> 生成导出 sum 的模块代码
lazy.ts @ lazy runtime
-> 生成异步 Chunk 中的模块代码
结果并不是立即写文件,而是写入 CodeGenerationResults。之后的 Chunk 渲染阶段才会将:
main Chunk 的模块代码 + main 所需 runtime modules
-> main.js
lazy Chunk 的模块代码
-> lazy.js
这一篇应该带走什么
这一阶段的主线是:
优化完成的 ModuleGraph + ChunkGraph
-> 按 module/runtime 收集 CodeGenerationJob
-> 相同 hash 的 runtime 合并 Job
-> 并行调用 Module::code_generation
-> 收集 source、runtime requirements 和 hash
-> 写入 CodeGenerationResults
-> 供后续 runtime 和 asset 阶段消费
从 Rust 设计角度,至少有四点值得记住:
- 按 runtime 而不是按文件缓存:同一模块不同执行上下文可能有不同代码;
- 先分组、再并行:在图结构稳定后,独立的 code generation job 可以安全并发;
- Capability declaration 而非直接拼 runtime:模块只收集
runtime_requirements,统一由后续阶段提供实现; - Artifact 作为阶段契约:
CodeGenerationResults将“生成模块代码”和“渲染最终文件”隔开,让后续 Pass 不必重新理解 Module 内部细节。
写在最后
至此,Rspack 已经完成了从源码模块到“按 runtime 生成的模块代码”的转换。距离 dist/main.js 只差最后几步:收集 runtime requirements、计算 hash、为 Chunk 创建 asset,再进入 process_assets 钩子。
下一篇可以继续追 RuntimeRequirementsPass 与 CreateChunkAssetsPass,重点看 runtime module 如何被加入 Chunk,以及多个 CodeGenerationResult 最终怎样被渲染为浏览器能加载的文件。