Skip to main content

rspack_plugin_split_chunks/plugin/
mod.rs

1mod 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    // Sort modules to ensure deterministic processing order.
62    // Use the precomputed identifier hash first to avoid repeated long string comparisons.
63    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        // sort by (group.index, chunk index in group)
78        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          // The chunk is not new but created in code splitting. We need remove `new_chunk` since we would remove
231          // modules in this `Chunk/ModuleGroup` from other chunks. Other chunks is stored in `ModuleGroup.chunks`.
232          module_group.chunks.remove(&new_chunk);
233        }
234
235        // If the module group size exceeds enforceSizeThreshold, skip maxRequest constraints
236        // https://webpack.js.org/plugins/split-chunks-plugin/#splitchunksenforcesizethreshold
237        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          // There are some chunks removed by `ensure_max_request_fit`
250          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            // `min_size` is not satisfied, ignore this invalid `ModuleGroup`
258            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            // return;
264          }
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}