本篇是 Rspack 源码解析系列第十七篇。第七篇讲到
splitChunks在OptimizeChunksPass阶段生效,但只停留在"为什么放在这里"。本文直接进入crates/rspack_plugin_split_chunks,看一个 cache group 如何把共享模块从多个 Chunk 中切出来、放进新 Chunk,并写回 ChunkGraph 和 incremental Mutation。
前言:splitChunks 不是一个排序算法
很多人以为 splitChunks 只是"把出现两次以上的模块拎出来"。真实实现要同时处理:
cache group 优先级
minSize / minChunks / maxSize / maxRequest
reuseExistingChunk
模块在不同 chunk 中的归属
maxInitialSize / maxAsyncSize 的二次拆分
chunk 与 chunk 之间的父子关系
incremental 的 Mutation 记录
这些逻辑都落在 crates/rspack_plugin_split_chunks 这个 crate 里。
源码入口:optimize_chunks Hook
rspack_plugin_split_chunks/src/plugin/mod.rs 是整个插件的入口。它的注册方式非常典型:
#[plugin_hook(CompilationOptimizeChunks for SplitChunksPlugin, stage = Compilation::OPTIMIZE_CHUNKS_STAGE_ADVANCED)]
async fn optimize_chunks(&self, compilation: &mut Compilation) -> Result<Option<bool>> {
self.inner_impl(compilation).await?;
compilation
.build_chunk_graph_artifact
.chunk_graph
.generate_dot(compilation, "after-split-chunks")
.await;
Ok(None)
}
impl Plugin for SplitChunksPlugin {
fn name(&self) -> &'static str {
"rspack.SplitChunksPlugin"
}
fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
ctx
.compilation_hooks
.optimize_chunks
.tap(optimize_chunks::new(self));
Ok(())
}
}
注意两点:
stage = Compilation::OPTIMIZE_CHUNKS_STAGE_ADVANCED——它故意排在其他 chunk 优化之后,因为 splitChunks 需要看到相对完整的 ChunkGraph;- 执行完后调用
generate_dot输出一张after-split-chunks的 dot 图,方便调试分包结果。
第一阶段:准备模块数据
inner_impl 一开始先收集三样东西:
let mut all_modules = compilation
.get_module_graph()
.modules_keys()
.copied()
.collect::<Vec<_>>();
// Sort modules to ensure deterministic processing order
all_modules.sort_unstable();
let module_sizes = get_module_sizes(all_modules.par_iter().copied(), compilation);
let module_chunks = Self::get_module_chunks(&all_modules, compilation);
all_modules.sort_unstable()保证处理顺序确定,多次构建结果一致;module_sizes用 rayon 并行计算每个模块的大小;module_chunks是module -> 它属于哪些 chunk的映射。
get_module_chunks 在 chunk.rs 里,直接用 rayon 并行读 ChunkGraph:
pub(crate) fn get_module_chunks(
all_modules: &[ModuleIdentifier],
compilation: &Compilation,
) -> ModuleChunks {
let chunk_graph = &compilation.build_chunk_graph_artifact.chunk_graph;
all_modules
.par_iter()
.map(|module| (*module, chunk_graph.get_module_chunks(*module).clone()))
.collect::<IdentifierMap<_>>()
}
这告诉我们:splitChunks 的输入不是原始文件,而是已经建好的 ChunkGraph 上的"模块—Chunk"关系。
第二阶段:把 cache groups 按 priority 排序
splitChunks.cacheGroups 里的每组有 priority。priority 越高的组越先匹配。源码先把它们按 priority 分组:
let mut priority_cache_groups = vec![];
for (priority, cache_groups) in &self
.cache_groups
.iter()
.enumerate()
.map(|v| IndexedCacheGroup {
cache_group_index: v.0,
cache_group: v.1,
})
.sorted_by(|a, b| match b.compare_by_priority(a) {
Ordering::Equal => a.compare_by_index(b),
v => v,
})
.chunk_by(|v| v.cache_group.priority)
{
priority_cache_groups.push((priority, cache_groups.into_iter().collect::<Vec<_>>()));
}
这里 compare_by_priority 先按 priority 降序,priority 相同时按 index 升序(先声明的优先)。chunk_by 把相同 priority 的组聚到一起。
第三阶段:决定模块分组方式
splitChunks 有两种分组维度,取决于 cacheGroups 是否配置了 usedExports:
if self.cache_groups.iter().any(|cg| !cg.used_exports) {
combinator.prepare_group_by_chunks(&all_modules, &module_chunks, &chunk_index_map);
}
if self.cache_groups.iter().any(|cg| cg.used_exports) {
combinator.prepare_group_by_used_exports(
&all_modules,
&compilation.exports_info_artifact,
&compilation.build_chunk_graph_artifact.chunk_by_ukey,
&module_chunks,
&chunk_index_map,
);
}
prepare_group_by_chunks:按"模块出现在哪些 chunk"分组(经典 splitChunks 行为);prepare_group_by_used_exports:按"模块被哪些 export 使用"分组,这是结合 tree shaking 的更细粒度分包。
两种方式共用一个 Combinator,输出 ModuleGroup 候选。
第四阶段:主循环,逐个取最优 ModuleGroup
核心是一个 while 循环:
while !module_group_map.is_empty() {
let (module_group_key, mut module_group) =
self.find_best_module_group(&mut module_group_map);
let cache_group = module_group.get_cache_group(&self.cache_groups);
let mut is_reuse_existing_chunk = false;
let mut is_reuse_existing_chunk_with_all_modules = false;
let new_chunk = self.get_corresponding_chunk(
compilation,
&mut module_group,
&mut is_reuse_existing_chunk,
&mut is_reuse_existing_chunk_with_all_modules,
);
// ...
}
find_best_module_group 会挑出当前优先级下"最值得切"的 ModuleGroup,处理完后从 map 中移除。这里的关键是 get_corresponding_chunk,它决定是复用已有 chunk 还是新建 chunk。
get_corresponding_chunk:三种去向
chunk.rs 里的 get_corresponding_chunk 分三种情况:
情况一:有 chunk name,且 named_chunks 里已有同名 chunk
if let Some(chunk_name) = &module_group.chunk_name {
if let Some(chunk) = compilation
.build_chunk_graph_artifact
.named_chunks
.get(chunk_name)
{
*is_reuse_existing_chunk = true;
*chunk
} else {
// 创建 named chunk
}
}
情况二:reuseExistingChunk 且存在可复用的 code splitting chunk
} else if module_group.cache_group_reuse_existing_chunk
&& let Some(reusable_chunk) = self.find_the_best_reusable_chunk(compilation, module_group)
{
*is_reuse_existing_chunk = true;
*is_reuse_existing_chunk_with_all_modules = true;
reusable_chunk
}
find_the_best_reusable_chunk 会找"已经包含同样模块集合"的 chunk,避免重复拆分。其中有一个 fast path:
if compilation
.build_chunk_graph_artifact
.chunk_graph
.get_number_of_chunk_modules(&chunk.ukey())
!= module_group.modules.len()
{
// Fast path
return None;
}
情况三:新建一个 chunk
let new_chunk_ukey =
Compilation::add_chunk(&mut compilation.build_chunk_graph_artifact.chunk_by_ukey);
if let Some(mut mutations) = compilation.incremental.mutations_write() {
mutations.add(Mutation::ChunkAdd { chunk: new_chunk_ukey });
}
注意新建 chunk 时会写入 Mutation::ChunkAdd,这是增量构建体系的一部分——后续 Pass 能据此知道"新增了哪些 chunk"。
移动模块:disconnect 再 connect
选定 chunk 后,move_modules_to_new_chunk_and_remove_from_old_chunks 负责把模块从旧 chunk 挪到新 chunk:
compilation
.build_chunk_graph_artifact
.chunk_graph
.disconnect_chunks_and_modules(&chunks, &modules);
compilation
.build_chunk_graph_artifact
.chunk_graph
.connect_chunk_and_modules(new_chunk, &modules);
它的本质是改 ChunkGraph 里的归属关系:先断开模块与旧 chunk 的连接,再连到新 chunk。移动前还会过滤掉 chunk_condition 不满足的模块:
let modules = item.modules.iter().filter(|mid| {
if let Some(module) = compilation.module_by_identifier(mid)
&& module.chunk_condition(&new_chunk, compilation).is_some_and(|c| !c)
{
return false;
}
true
}).copied().collect::<Vec<_>>();
建立 chunk 之间的 split 关系
模块挪走后,还要让旧 chunk 知道"我的一部分内容被拆到了 new_chunk"。split_from_original_chunks 调用 Chunk::split:
for original_chunk_ukey in original_chunks {
let [Some(new_chunk), Some(original_chunk)] = compilation
.build_chunk_graph_artifact
.chunk_by_ukey
.get_many_mut([&new_chunk_ukey, original_chunk_ukey])
else {
panic!("split_from_original_chunks failed")
};
original_chunk.split(
new_chunk,
&mut compilation.build_chunk_graph_artifact.chunk_group_by_ukey,
);
if let Some(mut mutations) = compilation.incremental.mutations_write() {
mutations.add(Mutation::ChunkSplit {
from: *original_chunk_ukey,
to: new_chunk_ukey,
});
}
}
这里又写入了 Mutation::ChunkSplit。它记录"from chunk 拆出了 to chunk",是后续 chunk 加载关系、runtime 生成的重要输入。
约束检查:maxRequest、minSize、maxSize
主循环里穿插着多个约束检查。例如 ensure_max_request_fit 会在 chunk 数量超过 maxInitialRequest/maxAsyncRequest 时减少参与拆分的 chunk 数:
let mut used_chunks = Cow::Borrowed(&module_group.chunks);
self.ensure_max_request_fit(compilation, cache_group, &mut used_chunks);
if used_chunks.len() != module_group.chunks.len() {
let used_chunks_len = if is_reuse_existing_chunk {
used_chunks.len() + 1
} else {
used_chunks.len()
};
if used_chunks_len < cache_group.min_chunks as usize {
continue;
}
}
ensure_min_size_fit 和 ensure_max_size_fit 则分别处理最小/最大体积约束。max_size 的拆分逻辑在 max_size.rs 里,是一个独立的二次切分流程(因为把一个超大 chunk 再拆小,需要额外维护 chunk 关系)。
Rust 角度:确定性 + 并行 + 增量记录
这整个插件最能体现 Rspack 核心的三个工程习惯:
- 确定性:
sort_unstable、sort_by_cached_key、best_reusable_chunk里的稳定比较,都是为了多次构建结果一致; - 并行:
get_module_sizes、get_module_chunks用par_iter,计算密集的部分交给 rayon; - 增量记录:新建 chunk 和拆分 chunk 都写
Mutation,让增量构建能精准失效。
这和第十一篇讲的 Artifact/Mutation/Cache 体系一脉相承:splitChunks 不只是改 ChunkGraph,还要把"改了什么"记录下来。
这一篇应该带走什么
- splitChunks 在
OptimizeChunksPass的 ADVANCED stage 执行,输入是已建好的 ChunkGraph; - cache groups 按 priority 排序,priority 相同按声明 index;
- 模块先按"chunk 归属"或"used exports"聚成 ModuleGroup,再逐个挑最优切分;
get_corresponding_chunk决定复用 named chunk、复用已有 chunk 还是新建 chunk;- 移动模块本质是 ChunkGraph 的 disconnect + connect,并写
Mutation::ChunkAdd/ChunkSplit支撑增量构建。
写在最后
splitChunks 决定了"哪些模块进哪些 chunk",但它能切得多细,取决于前面的 tree shaking 是否已经算清了每个模块真正被使用的导出。下一篇我们回到 rspack_plugin_javascript 的 SideEffectsFlagPlugin,看 tree shaking 如何通过"副作用连接状态"优化模块之间的引用关系。