diff --git a/crates/node_binding/binding.d.ts b/crates/node_binding/binding.d.ts index 79858673a7c2..d4b0c2704bc6 100644 --- a/crates/node_binding/binding.d.ts +++ b/crates/node_binding/binding.d.ts @@ -978,6 +978,7 @@ export interface RawCacheGroupOptions { name?: string | false | Function reuseExistingChunk?: boolean enforce?: boolean + usedExports?: boolean } export interface RawCacheGroupTestCtx { @@ -1662,6 +1663,7 @@ export interface RawSplitChunksOptions { cacheGroups?: Array /** What kind of chunks should be selected. */ chunks?: RegExp | 'async' | 'initial' | 'all' | Function + usedExports?: boolean automaticNameDelimiter?: string maxAsyncRequests?: number maxInitialRequests?: number diff --git a/crates/rspack_binding_options/src/options/raw_split_chunks/mod.rs b/crates/rspack_binding_options/src/options/raw_split_chunks/mod.rs index 1ff433851563..97325fdff9bc 100644 --- a/crates/rspack_binding_options/src/options/raw_split_chunks/mod.rs +++ b/crates/rspack_binding_options/src/options/raw_split_chunks/mod.rs @@ -37,6 +37,7 @@ pub struct RawSplitChunksOptions { #[napi(ts_type = "RegExp | 'async' | 'initial' | 'all' | Function")] #[derivative(Debug = "ignore")] pub chunks: Option, + pub used_exports: Option, pub automatic_name_delimiter: Option, pub max_async_requests: Option, pub max_initial_requests: Option, @@ -95,6 +96,7 @@ pub struct RawCacheGroupOptions { // used_exports: bool, pub reuse_existing_chunk: Option, pub enforce: Option, + pub used_exports: Option, } impl From for rspack_plugin_split_chunks::PluginOptions { @@ -220,6 +222,9 @@ impl From for rspack_plugin_split_chunks::PluginOptions { max_initial_size, r#type, layer, + used_exports: v + .used_exports + .unwrap_or_else(|| raw_opts.used_exports.unwrap_or_default()), } }), ); diff --git a/crates/rspack_core/src/exports_info.rs b/crates/rspack_core/src/exports_info.rs index 3e241bae7477..ba2d4e18e8ea 100644 --- a/crates/rspack_core/src/exports_info.rs +++ b/crates/rspack_core/src/exports_info.rs @@ -7,6 +7,7 @@ use std::sync::atomic::Ordering::Relaxed; use std::sync::Arc; use std::sync::LazyLock; +use either::Either; use itertools::Itertools; use rspack_collections::impl_item_ukey; use rspack_collections::Ukey; @@ -672,6 +673,33 @@ impl ExportsInfo { } } + pub fn get_usage_key(&self, mg: &ModuleGraph, runtime: Option<&RuntimeSpec>) -> UsageKey { + let exports_info = self.as_exports_info(mg); + + // only expand capacity when this has redirect_to + let mut key = UsageKey(Vec::with_capacity(exports_info.exports.len() + 2)); + + if let Some(redirect_to) = &exports_info.redirect_to { + key.add(Either::Left(Box::new( + redirect_to.get_usage_key(mg, runtime), + ))); + } else { + key.add(Either::Right( + self.other_exports_info(mg).get_used(mg, runtime), + )); + }; + + key.add(Either::Right( + exports_info.side_effects_only_info.get_used(mg, runtime), + )); + + for export_info in self.ordered_exports(mg) { + key.add(Either::Right(export_info.get_used(mg, runtime))); + } + + key + } + pub fn is_used(&self, mg: &ModuleGraph, runtime: Option<&RuntimeSpec>) -> bool { let info = self.as_exports_info(mg); if let Some(redirect_to) = info.redirect_to { @@ -1607,6 +1635,15 @@ pub struct FindTargetRetValue { pub export: Option>, } +#[derive(Debug, Hash, PartialEq, Eq, Default)] +pub struct UsageKey(pub(crate) Vec, UsageState>>); + +impl UsageKey { + fn add(&mut self, value: Either, UsageState>) { + self.0.push(value); + } +} + #[derive(Debug, Clone)] struct UnResolvedExportInfoTarget { connection: Option, diff --git a/crates/rspack_plugin_split_chunks/src/options/cache_group.rs b/crates/rspack_plugin_split_chunks/src/options/cache_group.rs index 0a70b490bddf..104130ac26f8 100644 --- a/crates/rspack_plugin_split_chunks/src/options/cache_group.rs +++ b/crates/rspack_plugin_split_chunks/src/options/cache_group.rs @@ -42,4 +42,5 @@ pub struct CacheGroup { pub max_initial_size: SplitChunkSizes, pub filename: Option, pub automatic_name_delimiter: String, + pub used_exports: bool, } diff --git a/crates/rspack_plugin_split_chunks/src/plugin/module_group.rs b/crates/rspack_plugin_split_chunks/src/plugin/module_group.rs index 63aa6fd5e5ca..cb7a046c9ad4 100644 --- a/crates/rspack_plugin_split_chunks/src/plugin/module_group.rs +++ b/crates/rspack_plugin_split_chunks/src/plugin/module_group.rs @@ -3,9 +3,13 @@ use std::hash::{BuildHasherDefault, Hash, Hasher}; use dashmap::DashMap; use rayon::prelude::*; -use rspack_collections::UkeySet; -use rspack_core::{Chunk, ChunkGraph, ChunkUkey, Compilation, Module, ModuleGraph}; +use rspack_collections::{IdentifierHasher, UkeyHasher, UkeySet}; +use rspack_core::{ + Chunk, ChunkByUkey, ChunkGraph, ChunkUkey, Compilation, Module, ModuleGraph, ModuleIdentifier, + UsageKey, +}; use rspack_error::Result; +use rspack_util::fx_hash::{FxDashMap, FxIndexMap}; use rustc_hash::{FxHashMap, FxHasher}; use super::ModuleGroupMap; @@ -36,6 +40,225 @@ impl Hasher for IdentityHasher { type ChunksKeyHashBuilder = BuildHasherDefault; +fn get_key<'a, I: Iterator>(chunks: I) -> ChunksKey { + let mut sorted_chunk_ukeys = chunks + .map(|chunk| { + // Increment each usize by 1 to avoid hashing the value 0 with FxHasher, which would always return a hash of 0 + chunk.as_u32() + 1 + }) + .collect::>(); + sorted_chunk_ukeys.sort_unstable(); + let mut hasher = FxHasher::default(); + for chunk_ukey in sorted_chunk_ukeys { + chunk_ukey.hash(&mut hasher); + } + hasher.finish() +} + +#[derive(Default)] +struct Combinator { + combinations_cache: FxDashMap>>, + used_exports_combinations_cache: FxDashMap>>, + + chunk_sets_in_graph: FxDashMap>, + chunk_sets_by_count: DashMap>, BuildHasherDefault>, + + used_exports_chunk_sets_in_graph: FxDashMap>, + used_exports_chunk_sets_by_count: + DashMap>, BuildHasherDefault>, + + grouped_by_exports: + DashMap>, BuildHasherDefault>, +} + +impl Combinator { + fn group_chunks_by_exports( + module_identifier: &ModuleIdentifier, + module_chunks: impl Iterator, + module_graph: &ModuleGraph, + chunk_by_ukey: &ChunkByUkey, + ) -> Vec> { + let exports_info = module_graph.get_exports_info(module_identifier); + let mut grouped_by_used_exports: FxHashMap> = Default::default(); + for chunk_ukey in module_chunks { + let chunk = chunk_by_ukey.expect_get(&chunk_ukey); + let usage_key = exports_info.get_usage_key(module_graph, Some(&chunk.runtime)); + + grouped_by_used_exports + .entry(usage_key) + .or_default() + .insert(chunk_ukey); + } + + grouped_by_used_exports.values().cloned().collect() + } + + fn get_combination( + &self, + chunks_key: ChunksKey, + combinations_cache: &FxDashMap>>, + chunk_sets_in_graph: &FxDashMap>, + chunk_sets_by_count: &DashMap>, BuildHasherDefault>, + ) -> Vec> { + match combinations_cache.entry(chunks_key) { + dashmap::mapref::entry::Entry::Occupied(entry) => entry.get().clone(), + dashmap::mapref::entry::Entry::Vacant(entry) => { + let chunks_set = chunk_sets_in_graph + .get(&chunks_key) + .expect("This should never happen, please file an issue"); + + let mut result = vec![chunks_set.value().clone()]; + + for item in chunk_sets_by_count.iter() { + let count = item.key(); + let array_of_set = item.value(); + + if *count < chunks_set.len() as u32 { + for set in array_of_set { + if set.is_subset(chunks_set.value()) { + result.push(set.clone()); + } + } + } + } + + entry.insert(result.clone()); + result + } + } + } + + fn get_combs( + &self, + module: ModuleIdentifier, + module_graph: &ModuleGraph, + chunk_graph: &ChunkGraph, + chunk_by_ukey: &ChunkByUkey, + used_exports: bool, + ) -> Vec> { + if used_exports { + let (chunk_sets_in_graph, chunk_sets_by_count) = + self.group_by_used_exports(module_graph, chunk_graph, chunk_by_ukey); + + let mut result = vec![]; + let chunks_by_module_used = self.grouped_by_exports.get(&module).unwrap(); + + for chunks in chunks_by_module_used.iter() { + let chunks_key = get_key(chunks.iter()); + let combs = self.get_combination( + chunks_key, + &self.used_exports_combinations_cache, + chunk_sets_in_graph, + chunk_sets_by_count, + ); + result.extend(combs.into_iter()); + } + + result + } else { + let (chunk_sets_in_graph, chunk_sets_by_count) = + self.group_by_chunks(module_graph, chunk_graph); + let chunks = chunk_graph.get_module_chunks(module); + self.get_combination( + get_key(chunks.iter()), + &self.combinations_cache, + chunk_sets_in_graph, + chunk_sets_by_count, + ) + } + } + + fn group_by_chunks( + &self, + module_graph: &ModuleGraph, + chunk_graph: &ChunkGraph, + ) -> ( + &FxDashMap>, + &DashMap>, BuildHasherDefault>, + ) { + if !self.chunk_sets_in_graph.is_empty() { + return (&self.chunk_sets_in_graph, &self.chunk_sets_by_count); + } + + let chunk_sets_in_graph = &self.chunk_sets_in_graph; + let chunk_sets_by_count = &self.chunk_sets_by_count; + + for module in module_graph.modules().keys() { + let chunks = chunk_graph.get_module_chunks(*module); + if chunks.is_empty() { + continue; + } + let chunk_key = get_key(chunks.iter()); + chunk_sets_in_graph.insert(chunk_key, chunks.clone()); + } + + for item in chunk_sets_in_graph.iter() { + let chunks = item.value(); + let count = chunks.len(); + + chunk_sets_by_count + .entry(count as u32) + .and_modify(|set| set.push(chunks.clone())) + .or_insert(vec![chunks.clone()]); + } + + (&self.chunk_sets_in_graph, &self.chunk_sets_by_count) + } + + fn group_by_used_exports( + &self, + module_graph: &ModuleGraph, + chunk_graph: &ChunkGraph, + chunk_by_ukey: &ChunkByUkey, + ) -> ( + &FxDashMap>, + &DashMap>, BuildHasherDefault>, + ) { + if !self.used_exports_chunk_sets_in_graph.is_empty() { + return ( + &self.used_exports_chunk_sets_in_graph, + &self.used_exports_chunk_sets_by_count, + ); + } + + let grouped_by_exports = &self.grouped_by_exports; + let used_exports_chunk_sets_in_graph = &self.used_exports_chunk_sets_in_graph; + let used_exports_chunk_sets_by_count = &self.used_exports_chunk_sets_by_count; + + for module in module_graph.modules().keys() { + let grouped_chunks = Self::group_chunks_by_exports( + module, + chunk_graph.get_module_chunks(*module).iter().cloned(), + module_graph, + chunk_by_ukey, + ); + for chunks in &grouped_chunks { + if chunks.is_empty() { + continue; + } + let chunk_key = get_key(chunks.iter()); + used_exports_chunk_sets_in_graph.insert(chunk_key, chunks.clone()); + } + + grouped_by_exports.insert(*module, grouped_chunks); + } + + for item in used_exports_chunk_sets_in_graph.iter() { + let chunks = item.value(); + let count = chunks.len(); + used_exports_chunk_sets_by_count + .entry(count as u32) + .and_modify(|set| set.push(chunks.clone())) + .or_insert(vec![chunks.clone()]); + } + + ( + used_exports_chunk_sets_in_graph, + used_exports_chunk_sets_by_count, + ) + } +} + impl SplitChunksPlugin { #[tracing::instrument(skip_all)] pub(crate) fn find_best_module_group( @@ -83,37 +306,7 @@ impl SplitChunksPlugin { let module_group_map: DashMap = DashMap::default(); - // chunk_sets_in_graph: key: module, value: multiple chunks contains the module - // single_chunk_sets: chunkset of module that belongs to only one chunk - // chunk_sets_by_count: use chunkset len as key - let (chunk_sets_in_graph, chunk_sets_by_count) = - { Self::prepare_combination_maps(&module_graph, &compilation.chunk_graph) }; - - let combinations_cache = - DashMap::>, ChunksKeyHashBuilder>::default(); - - let get_combination = |chunks_key: ChunksKey| match combinations_cache.entry(chunks_key) { - dashmap::mapref::entry::Entry::Occupied(entry) => entry.get().clone(), - dashmap::mapref::entry::Entry::Vacant(entry) => { - let chunks_set = chunk_sets_in_graph - .get(&chunks_key) - .expect("This should never happen, please file an issue"); - let mut result = vec![chunks_set.clone()]; - - for (count, array_of_set) in &chunk_sets_by_count { - if *count < chunks_set.len() { - for set in array_of_set { - if set.is_subset(chunks_set) { - result.push(set.clone()); - } - } - } - } - - entry.insert(result.clone()); - result - } - }; + let combinator = Combinator::default(); module_graph.modules().values().par_bridge().map(|module| { let module = &***module; @@ -126,8 +319,6 @@ impl SplitChunksPlugin { return Ok(()); } - let chunks_key = Self::get_key(belong_to_chunks.iter()); - let mut temp = Vec::with_capacity(self.cache_groups.len()); for idx in 0..self.cache_groups.len() { @@ -171,7 +362,13 @@ impl SplitChunksPlugin { .filter(|(index, _)| temp[*index]); for (cache_group_index, (idx, cache_group)) in filtered.enumerate() { - let combs = get_combination(chunks_key); + let combs = combinator.get_combs( + module.identifier(), + &module_graph, + &compilation.chunk_graph, + &compilation.chunk_by_ukey, + cache_group.used_exports + ); for chunk_combination in combs { if chunk_combination.is_empty() { @@ -221,7 +418,7 @@ impl SplitChunksPlugin { } let selected_chunks_key = - { Self::get_key(selected_chunks.iter().map(|chunk| &chunk.ukey)) }; + { get_key(selected_chunks.iter().map(|chunk| &chunk.ukey)) }; merge_matched_item_into_module_group_map( MatchedItem { @@ -389,48 +586,63 @@ impl SplitChunksPlugin { }); } - fn get_key<'a, I: Iterator>(chunks: I) -> ChunksKey { - let mut sorted_chunk_ukeys = chunks - .map(|chunk| { - // Increment each usize by 1 to avoid hashing the value 0 with FxHasher, which would always return a hash of 0 - chunk.as_u32() + 1 - }) - .collect::>(); - sorted_chunk_ukeys.sort_unstable(); - let mut hasher = FxHasher::default(); - for chunk_ukey in sorted_chunk_ukeys { - chunk_ukey.hash(&mut hasher); - } - hasher.finish() - } - - #[allow(clippy::type_complexity)] - fn prepare_combination_maps( - module_graph: &ModuleGraph, - chunk_graph: &ChunkGraph, - ) -> ( - HashMap, ChunksKeyHashBuilder>, - FxHashMap>>, - ) { - let mut chunk_sets_in_graph = - HashMap::, ChunksKeyHashBuilder>::default(); - - for module in module_graph.modules().keys() { - let chunks = chunk_graph.get_module_chunks(*module); - let chunk_key = Self::get_key(chunks.iter()); - chunk_sets_in_graph.insert(chunk_key, chunks.clone()); - } - - let mut chunk_sets_by_count = FxHashMap::>>::default(); - - for chunks in chunk_sets_in_graph.values() { - let count = chunks.len(); - chunk_sets_by_count - .entry(count) - .and_modify(|set| set.push(chunks.clone())) - .or_insert(vec![chunks.clone()]); - } - - (chunk_sets_in_graph, chunk_sets_by_count) - } + // #[allow(clippy::type_complexity)] + // fn prepare_combination_maps( + // module_graph: &ModuleGraph, + // chunk_graph: &ChunkGraph, + // used_exports: bool, + // chunk_by_ukey: &ChunkByUkey, + // ) -> ( + // HashMap, ChunksKeyHashBuilder>, + // FxHashMap>>, + // Option>>>, + // ) { + // let mut chunk_sets_in_graph = + // HashMap::, ChunksKeyHashBuilder>::default(); + // let mut chunk_sets_by_count = FxHashMap::>>::default(); + + // let mut grouped_by_exports_map: Option>>> = + // None; + + // if used_exports { + // let mut grouped_by_exports: FxHashMap>> = + // Default::default(); + // for module in module_graph.modules().keys() { + // let grouped_chunks = Self::group_chunks_by_exports( + // module, + // chunk_graph.get_module_chunks(*module).iter().cloned(), + // module_graph, + // chunk_by_ukey, + // ); + // for chunks in &grouped_chunks { + // let chunk_key = get_key(chunks.iter()); + // chunk_sets_in_graph.insert(chunk_key, chunks.clone()); + // } + + // grouped_by_exports.insert(*module, grouped_chunks); + // } + + // grouped_by_exports_map = Some(grouped_by_exports); + // } else { + // for module in module_graph.modules().keys() { + // let chunks = chunk_graph.get_module_chunks(*module); + // let chunk_key = get_key(chunks.iter()); + // chunk_sets_in_graph.insert(chunk_key, chunks.clone()); + // } + // } + + // for chunks in chunk_sets_in_graph.values() { + // let count = chunks.len(); + // chunk_sets_by_count + // .entry(count) + // .and_modify(|set| set.push(chunks.clone())) + // .or_insert(vec![chunks.clone()]); + // } + + // ( + // chunk_sets_in_graph, + // chunk_sets_by_count, + // grouped_by_exports_map, + // ) + // } } diff --git a/packages/rspack/src/config/defaults.ts b/packages/rspack/src/config/defaults.ts index 10d8bccfba6f..6d4a851488ab 100644 --- a/packages/rspack/src/config/defaults.ts +++ b/packages/rspack/src/config/defaults.ts @@ -937,7 +937,7 @@ const applyOptimizationDefaults = ( ); D(splitChunks, "hidePathInfo", production); D(splitChunks, "chunks", "async"); - // D(splitChunks, "usedExports", optimization.usedExports === true); + D(splitChunks, "usedExports", optimization.usedExports === true); D(splitChunks, "minChunks", 1); F(splitChunks, "minSize", () => (production ? 20000 : 10000)); // F(splitChunks, "minRemainingSize", () => (development ? 0 : undefined)); diff --git a/packages/rspack/src/config/zod.ts b/packages/rspack/src/config/zod.ts index 0c879c959faf..3ee1ebf665c3 100644 --- a/packages/rspack/src/config/zod.ts +++ b/packages/rspack/src/config/zod.ts @@ -1237,6 +1237,7 @@ const sharedOptimizationSplitChunksCacheGroup = { chunks: optimizationSplitChunksChunks.optional(), defaultSizeTypes: optimizationSplitChunksDefaultSizeTypes.optional(), minChunks: z.number().min(1).optional(), + usedExports: z.boolean().optional(), name: optimizationSplitChunksName.optional(), minSize: optimizationSplitChunksSizes.optional(), maxSize: optimizationSplitChunksSizes.optional(),