rspack_plugin_split_chunks/plugin/
mod.rs1mod chunk;
2mod max_request;
3pub mod max_size;
4pub mod min_size;
5mod module_group;
6
7use std::{borrow::Cow, cmp::Ordering, fmt::Debug};
8
9use itertools::Itertools;
10use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
11use rspack_collections::IdentifierMap;
12use rspack_core::{ChunkUkey, Compilation, CompilationOptimizeChunks, Logger, Plugin};
13use rspack_error::Result;
14use rspack_hook::{plugin, plugin_hook};
15use rspack_util::{fx_hash::FxIndexMap, tracing_preset::TRACING_BENCH_TARGET};
16use rustc_hash::{FxHashMap, FxHashSet};
17use tracing::instrument;
18
19use crate::{
20 CacheGroup, SplitChunkSizes,
21 common::FallbackCacheGroup,
22 get_module_sizes,
23 module_group::{IndexedCacheGroup, ModuleGroup, ModuleGroupKey},
24};
25
26type ModuleGroupMap = FxIndexMap<ModuleGroupKey, ModuleGroup>;
27
28#[derive(Debug)]
29pub struct PluginOptions {
30 pub cache_groups: Vec<CacheGroup>,
31 pub fallback_cache_group: FallbackCacheGroup,
32 pub hide_path_info: Option<bool>,
33}
34
35#[plugin]
36pub struct SplitChunksPlugin {
37 cache_groups: Box<[CacheGroup]>,
38 fallback_cache_group: FallbackCacheGroup,
39 hide_path_info: bool,
40}
41
42impl SplitChunksPlugin {
43 pub fn new(options: PluginOptions) -> Self {
44 tracing::debug!("Create `SplitChunksPlugin` with {:#?}", options);
45 Self::new_inner(
46 options.cache_groups.into(),
47 options.fallback_cache_group,
48 options.hide_path_info.unwrap_or(false),
49 )
50 }
51 #[instrument(name = "Compilation:SplitChunks",target=TRACING_BENCH_TARGET, skip_all)]
52 async fn inner_impl(&self, compilation: &mut Compilation) -> Result<()> {
53 let logger = compilation.get_logger(self.name());
54 let start = logger.time("prepare module data");
55
56 let mut all_modules = compilation
57 .get_module_graph()
58 .modules_keys()
59 .copied()
60 .collect::<Vec<_>>();
61 all_modules.sort_unstable_by_key(|module| (module.precomputed_hash(), *module));
64
65 let module_sizes = get_module_sizes(all_modules.par_iter().copied(), compilation);
66 let module_chunks = Self::get_module_chunks(&all_modules, compilation);
67 logger.time_end(start);
68
69 let chunk_index_map: FxHashMap<ChunkUkey, u32> = {
70 let mut ordered_chunks = compilation
71 .build_chunk_graph_artifact
72 .chunk_by_ukey
73 .values()
74 .collect::<Vec<_>>();
75
76 ordered_chunks.sort_by_cached_key(|chunk| {
77 let group = chunk
79 .groups()
80 .iter()
81 .map(|group| {
82 compilation
83 .build_chunk_graph_artifact
84 .chunk_group_by_ukey
85 .expect_get(group)
86 })
87 .min_by(|group1, group2| group1.index.cmp(&group2.index))
88 .expect("chunk should have at least one group");
89 let chunk_index = group
90 .chunks
91 .iter()
92 .position(|c| *c == chunk.ukey())
93 .expect("chunk should be in its group");
94 (group.index, chunk_index)
95 });
96
97 ordered_chunks
98 .iter()
99 .enumerate()
100 .map(|(index, chunk)| {
101 (
102 chunk.ukey(),
103 u32::try_from(index + 1).expect("chunk index should fit in u32"),
104 )
105 })
106 .collect()
107 };
108
109 let start = logger.time("prepare cache groups");
110 let mut priority_cache_groups = vec![];
111
112 for (priority, cache_groups) in &self
113 .cache_groups
114 .iter()
115 .enumerate()
116 .map(|v| IndexedCacheGroup {
117 cache_group_index: u32::try_from(v.0).expect("cache group index should fit in u32"),
118 cache_group: v.1,
119 })
120 .sorted_by(|a, b| match b.compare_by_priority(a) {
121 Ordering::Equal => a.compare_by_index(b),
122 v => v,
123 })
124 .chunk_by(|v| v.cache_group.priority)
125 {
126 priority_cache_groups.push((priority, cache_groups.into_iter().collect::<Vec<_>>()));
127 }
128
129 let mut max_size_setting_map: FxHashMap<ChunkUkey, MaxSizeSetting> = Default::default();
130 let mut removed_module_chunks: IdentifierMap<FxHashSet<ChunkUkey>> = IdentifierMap::default();
131
132 let mut combinator = module_group::Combinator::default();
133
134 let non_used_exports_min_chunks = self
135 .cache_groups
136 .iter()
137 .filter(|cache_group| !cache_group.used_exports)
138 .map(|cache_group| cache_group.min_chunks as usize)
139 .min();
140
141 if let Some(min_chunks) = non_used_exports_min_chunks {
142 combinator.prepare_group_by_chunks(
143 &all_modules,
144 &module_chunks,
145 &chunk_index_map,
146 min_chunks,
147 );
148 }
149
150 if self
151 .cache_groups
152 .iter()
153 .any(|cache_group| cache_group.used_exports)
154 {
155 combinator.prepare_group_by_used_exports(
156 &all_modules,
157 &compilation.exports_info_artifact,
158 &compilation.build_chunk_graph_artifact.chunk_by_ukey,
159 &module_chunks,
160 &chunk_index_map,
161 );
162 }
163
164 logger.time_end(start);
165
166 let start = logger.time("process cache groups");
167 let priority_len = priority_cache_groups.len();
168 for (index, (_, cache_groups)) in priority_cache_groups.into_iter().enumerate() {
169 let mut module_group_map = self
170 .prepare_module_group_map(
171 &combinator,
172 &all_modules,
173 cache_groups,
174 &removed_module_chunks,
175 compilation,
176 &module_chunks,
177 &chunk_index_map,
178 )
179 .await?;
180 tracing::trace!("prepared module_group_map {:#?}", module_group_map);
181
182 module_group_map
183 .par_iter_mut()
184 .for_each(|(_, module_group)| module_group.prepare_modules_for_sizes_and_compare());
185 self.ensure_min_size_fit(&mut module_group_map, &module_sizes);
186
187 while !module_group_map.is_empty() {
188 let (module_group_key, mut module_group) =
189 self.find_best_module_group(&mut module_group_map);
190
191 tracing::trace!(
192 "ModuleGroup({}) wins, {:?} `ModuleGroup` remains",
193 module_group_key,
194 module_group_map.len(),
195 );
196 let cache_group = module_group.get_cache_group(&self.cache_groups);
197
198 let mut is_reuse_existing_chunk = false;
199 let mut is_reuse_existing_chunk_with_all_modules = false;
200 let new_chunk = self.get_corresponding_chunk(
201 compilation,
202 &mut module_group,
203 &mut is_reuse_existing_chunk,
204 &mut is_reuse_existing_chunk_with_all_modules,
205 );
206
207 let new_chunk_mut = compilation
208 .build_chunk_graph_artifact
209 .chunk_by_ukey
210 .expect_get_mut(&new_chunk);
211 tracing::trace!(
212 "{module_group_key}, get Chunk {:?} with is_reuse_existing_chunk: {is_reuse_existing_chunk:?} and {is_reuse_existing_chunk_with_all_modules:?}",
213 new_chunk_mut.chunk_reason()
214 );
215
216 if let Some(chunk_reason) = new_chunk_mut.chunk_reason_mut() {
217 chunk_reason.push_str(&format!(" (cache group: {})", cache_group.key.as_str()));
218 if let Some(chunk_name) = &module_group.chunk_name {
219 chunk_reason.push_str(&format!(" (name: {chunk_name})"));
220 }
221 }
222
223 if let Some(filename) = &cache_group.filename {
224 new_chunk_mut.set_filename_template(Some(filename.clone()));
225 }
226
227 new_chunk_mut.add_id_name_hints(cache_group.id_hint.clone());
228
229 if is_reuse_existing_chunk {
230 module_group.chunks.remove(&new_chunk);
233 }
234
235 let enforce_size_exceeded = !cache_group.enforce_size_threshold.is_empty()
238 && module_group
239 .get_sizes(&module_sizes)
240 .bigger_than(&cache_group.enforce_size_threshold);
241
242 let mut used_chunks = Cow::Borrowed(&module_group.chunks);
243
244 if !enforce_size_exceeded {
245 self.ensure_max_request_fit(compilation, cache_group, &mut used_chunks);
246 }
247
248 if used_chunks.len() != module_group.chunks.len() {
249 let used_chunks_len = if is_reuse_existing_chunk {
251 used_chunks.len() + 1
252 } else {
253 used_chunks.len()
254 };
255
256 if used_chunks_len < cache_group.min_chunks as usize {
257 tracing::trace!(
259 "ModuleGroup({module_group_key}) is skipped. Reason: used_chunks_len({used_chunks_len:?}) < cache_group.min_chunks({:?})",
260 cache_group.min_chunks
261 );
262 continue;
263 }
265 }
266
267 if !cache_group.max_initial_size.is_empty() || !cache_group.max_async_size.is_empty() {
268 max_size_setting_map.insert(
269 new_chunk,
270 MaxSizeSetting {
271 min_size: cache_group.min_size.clone(),
272 max_async_size: cache_group.max_async_size.clone(),
273 max_initial_size: cache_group.max_initial_size.clone(),
274 automatic_name_delimiter: cache_group.automatic_name_delimiter.clone(),
275 },
276 );
277 }
278
279 self.move_modules_to_new_chunk_and_remove_from_old_chunks(
280 &module_group,
281 new_chunk,
282 &used_chunks,
283 compilation,
284 );
285
286 self.split_from_original_chunks(&module_group, &used_chunks, new_chunk, compilation);
287
288 self.remove_all_modules_from_other_module_groups(
289 &module_group,
290 &mut module_group_map,
291 &used_chunks,
292 compilation,
293 &module_sizes,
294 );
295
296 if index != priority_len - 1 {
297 for module in module_group.modules.iter() {
298 removed_module_chunks
299 .entry(*module)
300 .or_default()
301 .extend(module_group.chunks.iter().copied());
302 }
303 }
304 }
305 }
306 logger.time_end(start);
307
308 let start = logger.time("ensure max size fit");
309 self
310 .ensure_max_size_fit(compilation, &max_size_setting_map)
311 .await?;
312 logger.time_end(start);
313
314 rayon::spawn(move || drop(combinator));
315
316 Ok(())
317 }
318}
319
320impl Debug for SplitChunksPlugin {
321 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322 f.debug_struct("SplitChunksPlugin").finish()
323 }
324}
325
326#[plugin_hook(CompilationOptimizeChunks for SplitChunksPlugin, stage = Compilation::OPTIMIZE_CHUNKS_STAGE_ADVANCED)]
327async fn optimize_chunks(&self, compilation: &mut Compilation) -> Result<Option<bool>> {
328 self.inner_impl(compilation).await?;
329 Ok(None)
330}
331
332impl Plugin for SplitChunksPlugin {
333 fn name(&self) -> &'static str {
334 "rspack.SplitChunksPlugin"
335 }
336
337 fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
338 ctx
339 .compilation_hooks
340 .optimize_chunks
341 .tap(optimize_chunks::new(self));
342 Ok(())
343 }
344}
345
346#[derive(Debug)]
347struct MaxSizeSetting {
348 pub min_size: SplitChunkSizes,
349 pub max_async_size: SplitChunkSizes,
350 pub max_initial_size: SplitChunkSizes,
351 pub automatic_name_delimiter: String,
352}