前言
先把几个阶段的输出区分开:
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:
<span>// code_generation/mod.rs(简化)</span>
<span>async</span> <span>fn</span> <span>code_generation_pass_impl</span>(compilation: &<span>mut</span> Compilation) <span>-></span> <span>Result</span><()> {
<span>let</span> <span></span><span>code_generation_modules</span> = <span>if</span> <span>let</span> <span></span><span>Some</span>(mutations) = compilation
.incremental
.<span>mutations_read</span>(IncrementalPasses::MODULES_CODEGEN)
&& !compilation.code_generation_results.<span>is_empty</span>()
{
<span>// 删除已移除模块的旧结果</span>
<span>// 根据 ModuleSetHashes mutation 收集受影响模块</span>
<span>// 只重生成这些模块</span>
modules
} <span>else</span> {
*compilation.code_generation_results = <span>Default</span>::<span>default</span>();
compilation.<span>get_module_graph</span>().<span>modules_keys</span>().<span>copied</span>().<span>collect</span>()
};
compilation.<span>code_generation</span>(code_generation_modules).<span>await</span>?;
compilation.plugin_driver
.compilation_hooks
.after_code_generation
.<span>call</span>(compilation, &<span>mut</span> diagnostics)
.<span>await</span>?;
<span>Ok</span>(())
}
首次构建时,所有模块都会进入 code generation。增量构建时,Rspack 利用前面阶段记录的 Mutation::ModuleSetHashes 缩小范围:模块没有变化、生成 hash 没有失效,就无需重新生成。
这与上一篇的 Cutout + repair 是同一种思路:整个编译器不是“文件一变就重做所有阶段”,而是让每个 Artifact 都有自己的变更边界。
为什么要按 runtime 分组
Compilation::code_generation() 并不会立即遍历所有模块调用生成函数。它先区分有无 code generation dependencies 的模块,再交给 code_generation_modules():
<span>// code_generation/mod.rs(简化)</span>
<span>for</span> <span>runtime</span> <span>in</span> chunk_graph.<span>get_module_runtimes_iter</span>(
module,
&<span>self</span>.build_chunk_graph_artifact.chunk_by_ukey,
) {
<span>let</span> <span></span><span>hash</span> = ChunkGraph::<span>get_module_hash</span>(<span>self</span>, module, runtime)?;
<span>if</span> <span>let</span> <span></span><span>Some</span>(job) = map.<span>get_mut</span>(hash) {
job.runtimes.<span>push</span>(runtime.<span>clone</span>());
} <span>else</span> {
map.<span>insert</span>(hash.<span>clone</span>(), CodeGenerationJob {
module,
hash,
runtime: runtime.<span>clone</span>(),
runtimes: <span>vec!</span>[runtime.<span>clone</span>()],
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 并发执行:
<span>// code_generation/mod.rs(简化)</span>
<span>let</span> <span></span><span>results</span> = rspack_futures::scope::<_, _>(|token| {
jobs.<span>into_iter</span>().<span>for_each</span>(|job| {
<span>let</span> <span></span><span>s</span> = <span>unsafe</span> { token.<span>used</span>((&<span>self</span>, &module_graph, job)) };
s.<span>spawn</span>(|(this, module_graph, job)| <span>async</span> {
<span>let</span> <span></span><span>module</span> = module_graph
.<span>module_by_identifier</span>(&job.module)
.<span>expect</span>(<span>"should have module"</span>);
module.<span>code_generation</span>(&<span>mut</span> code_generation_context).<span>await</span>
})
})
})
.<span>await</span>;
和第三篇 Build Module Graph 的后台任务设计形成对照:
- 模块图构建时,共享图结构会被串行写入,因此需要把
AddTask等操作放回主任务队列; - 代码生成时,输入的 ModuleGraph、ChunkGraph 和前序优化结果已经稳定,每个 Job 产出独立的
CodeGenerationResult,因此适合并行计算。
这体现了一个重要的并发设计原则:不是所有编译阶段都需要同一种并发模型;能否并行取决于任务是否写同一份共享状态。
ModuleCodeGenerationContext:模块生成时能看到什么
每个 Job 会创建 ModuleCodeGenerationContext:
<span>// code_generation/mod.rs(简化)</span>
<span>let</span> <span>mut </span><span>runtime_template</span> = this.runtime_template.<span>create_module_code_template</span>();
<span>let</span> <span>mut </span><span>code_generation_context</span> = ModuleCodeGenerationContext {
compilation: this,
runtime: <span>Some</span>(&job.runtime),
concatenation_scope: job.scope.<span>clone</span>(),
runtime_template: &<span>mut</span> runtime_template,
};
module.<span>code_generation</span>(&<span>mut</span> code_generation_context).<span>await</span>
模块生成代码时不应该自己持有整个编译器的可变状态,而是通过这个上下文读取所需能力:
| 字段 | 用途 |
|---|---|
| `compilation` | 读取 options、ModuleGraph、ChunkGraph、插件钩子等编译信息 |
| `runtime` | 按当前 runtime 判断依赖连接、导出名和运行时代码需求 |
| `concatenation_scope` | 支持 Module Concatenation 等优化后的模块组合 |
| `runtime_template` | 生成 `__webpack_require__` 等 runtime 调用模板,并收集 runtime requirements |
这是很典型的 Rust API 设计:将一次操作实际需要的上下文显式封装为参数,而不是让模块对象依赖全局状态。
NormalModule 的代码生成发生了什么
不同模块类型各自实现 Module trait 中的 code_generation():
<span>// crates/rspack_core/src/module.rs(示意)</span>
<span>#[async_trait]</span>
<span>pub</span> <span>trait</span> <span>Module</span> {
<span>async</span> <span>fn</span> <span>code_generation</span>(
&<span>self</span>,
context: &<span>mut</span> ModuleCodeGenerationContext,
) <span>-></span> <span>Result</span><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
-> 浏览器运行时可调用的模块源码
例如源码:
<span>import</span> { sum } <span>from</span> <span>'./math'</span>;
<span>console</span>.<span>log</span>(<span>sum</span>(<span>1</span>, <span>2</span>));
经过模块化代码生成后,不再只是原始 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:
<span>// code_generation/mod.rs(简化)</span>
module.<span>code_generation</span>(&<span>mut</span> code_generation_context)
.<span>await</span>
.<span>map</span>(|<span>mut</span> codegen_res| {
codegen_res.runtime_requirements.<span>extend</span>(
*runtime_template.<span>runtime_requirements</span>()
);
codegen_res.<span>set_hash</span>(
&options.output.hash_function,
&options.output.hash_digest,
&options.output.hash_salt,
);
codegen_res
})
缓存:模块代码生成结果如何复用
生成代码前,Rspack 会通过 code_generate_cache_artifact.use_cache() 查询缓存:
<span>// code_generation/mod.rs(简化)</span>
<span>let</span> <span></span><span>codegen_res</span> = this
.code_generate_cache_artifact
.<span>use_cache</span>(&job, || <span>async</span> {
module.<span>code_generation</span>(&<span>mut</span> code_generation_context).<span>await</span>
})
.<span>await</span>;
缓存键对应 CodeGenerationJob,因此自然包含模块、hash 和 runtime 相关信息。对于未变更模块,缓存命中时就可以避免重复执行 dependency template 和源码转换。
同时,代码生成错误不会让 Rspack 在这里立刻丢弃所有其他模块结果:
<span>let</span> <span></span><span>codegen_res</span> = <span>match</span> codegen_res {
<span>Ok</span>(codegen_res) => codegen_res,
<span>Err</span>(err) => {
<span>let</span> <span>mut </span><span>diagnostic</span> = Diagnostic::<span>from</span>(err);
diagnostic.module_identifier = <span>Some</span>(module);
<span>self</span>.<span>push_diagnostic</span>(diagnostic);
CodeGenerationResult::<span>default</span>()
}
};
这延续了第三篇 Factorize/Build 阶段的设计:尽可能收集完整 diagnostics,最后统一向用户报告,而不是遇到第一个模块失败就停止整个编译流程。
结果如何写回 Compilation
每个并发 Job 完成后,主流程将结果写回 code_generation_results:
<span>// code_generation/mod.rs(简化)</span>
<span>self</span>
.code_generation_results
.<span>insert</span>(module, codegen_res, runtimes);
<span>self</span>.code_generated_modules.<span>insert</span>(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。
用前三篇的示例走一遍
继续使用:
<span>// index.ts</span>
<span>import</span> { sum } <span>from</span> <span>'./math'</span>;
<span>import</span>(<span>'./lazy'</span>);
<span>console</span>.<span>log</span>(<span>sum</span>(<span>1</span>, <span>2</span>));
在上一阶段,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 最终怎样被渲染为浏览器能加载的文件。
这篇把按 runtime 缓存、分组并发、runtime requirement 声明等设计讲清了,适合想理解 Rspack 编译性能与 Pass 解耦的前端构建开发者。