rspack_plugin_remove_duplicate_modules/
lib.rs1use std::sync::Arc;
2
3use rayon::prelude::*;
4use rspack_collections::IdentifierSet;
5use rspack_core::{
6 ChunkUkey, Compilation, CompilationOptimizeChunks, ModuleIdentifier, Plugin,
7 incremental::Mutation,
8};
9use rspack_error::Result;
10use rspack_hook::{plugin, plugin_hook};
11use rspack_util::fx_hash::FxDashMap;
12
13#[derive(Debug)]
14#[plugin]
15pub struct RemoveDuplicateModulesPlugin {}
16
17impl std::default::Default for RemoveDuplicateModulesPlugin {
18 fn default() -> Self {
19 Self {
20 inner: Arc::new(RemoveDuplicateModulesPluginInner {}),
21 }
22 }
23}
24
25fn find_reusable_chunk(
26 compilation: &Compilation,
27 chunks: &[ChunkUkey],
28 modules: &[ModuleIdentifier],
29) -> Option<ChunkUkey> {
30 let filter = |chunk: &&ChunkUkey| {
31 let chunk_modules = compilation
32 .build_chunk_graph_artifact
33 .chunk_graph
34 .get_chunk_modules_identifier(chunk);
35 modules.len() == chunk_modules.len()
36 && modules.iter().all(|module| chunk_modules.contains(module))
37 };
38
39 if chunks.len() > 10 {
40 chunks.par_iter().find_first(filter).copied()
41 } else {
42 chunks.iter().find(filter).copied()
43 }
44}
45
46#[plugin_hook(CompilationOptimizeChunks for RemoveDuplicateModulesPlugin)]
47async fn optimize_chunks(&self, compilation: &mut Compilation) -> Result<Option<bool>> {
48 let module_graph = compilation.get_module_graph();
49 let chunk_graph = &compilation.build_chunk_graph_artifact.chunk_graph;
50
51 let chunk_map: FxDashMap<Vec<ChunkUkey>, Vec<ModuleIdentifier>> = FxDashMap::default();
52
53 module_graph.modules_par().for_each(|(identifier, _)| {
54 let chunks = chunk_graph.get_module_chunks(*identifier);
55 let mut sorted_chunks = chunks.iter().copied().collect::<Vec<_>>();
56 sorted_chunks.sort();
57 chunk_map
58 .entry(sorted_chunks)
59 .or_default()
60 .push(*identifier);
61 });
62
63 let mut chunk_map = chunk_map.into_iter().collect::<Vec<_>>();
88 chunk_map.sort_by_key(|(chunks, _)| chunks.len());
89
90 for (chunks, modules) in chunk_map.into_iter().rev() {
91 if chunks.len() <= 1 {
92 continue;
93 }
94
95 let new_chunk_ukey = if let Some(chunk) = find_reusable_chunk(compilation, &chunks, &modules) {
97 chunk
101 } else {
102 let new_chunk_ukey =
103 Compilation::add_chunk(&mut compilation.build_chunk_graph_artifact.chunk_by_ukey);
104 if let Some(mut mutations) = compilation.incremental.mutations_write() {
105 mutations.add(Mutation::ChunkAdd {
106 chunk: new_chunk_ukey,
107 });
108 };
109 let new_chunk = compilation
110 .build_chunk_graph_artifact
111 .chunk_by_ukey
112 .expect_get_mut(&new_chunk_ukey);
113 *new_chunk.chunk_reason_mut() = Some("modules are shared across multiple chunks".into());
114 compilation
115 .build_chunk_graph_artifact
116 .chunk_graph
117 .add_chunk(new_chunk_ukey);
118
119 new_chunk_ukey
120 };
121
122 let mut entry_modules = IdentifierSet::default();
123
124 for chunk_ukey in &chunks {
125 if chunk_ukey == &new_chunk_ukey {
126 continue;
127 }
128
129 let [Some(new_chunk), Some(origin)] = compilation
130 .build_chunk_graph_artifact
131 .chunk_by_ukey
132 .get_many_mut([&new_chunk_ukey, chunk_ukey])
133 else {
134 panic!("should have both chunks")
135 };
136 entry_modules.extend(
137 compilation
138 .build_chunk_graph_artifact
139 .chunk_graph
140 .get_chunk_entry_modules(chunk_ukey),
141 );
142 origin.split(
143 new_chunk,
144 &mut compilation.build_chunk_graph_artifact.chunk_group_by_ukey,
145 );
146 if let Some(mut mutations) = compilation.incremental.mutations_write() {
147 mutations.add(Mutation::ChunkSplit {
148 from: *chunk_ukey,
149 to: new_chunk_ukey,
150 });
151 }
152 }
153
154 for m in modules {
155 let is_entry = entry_modules.contains(&m);
156 for chunk_ukey in &chunks {
157 if chunk_ukey == &new_chunk_ukey {
158 continue;
159 }
160 compilation
161 .build_chunk_graph_artifact
162 .chunk_graph
163 .disconnect_chunk_and_module(chunk_ukey, m);
164
165 if is_entry {
166 compilation
167 .build_chunk_graph_artifact
168 .chunk_graph
169 .disconnect_chunk_and_entry_module(chunk_ukey, m);
170 }
171 }
172
173 compilation
174 .build_chunk_graph_artifact
175 .chunk_graph
176 .connect_chunk_and_module(new_chunk_ukey, m);
177
178 if is_entry {
179 let chunk = compilation
180 .build_chunk_graph_artifact
181 .chunk_by_ukey
182 .expect_get(&new_chunk_ukey);
183 for group in chunk.groups().iter().filter(|group| {
184 let group = compilation
185 .build_chunk_graph_artifact
186 .chunk_group_by_ukey
187 .expect_get(group);
188
189 group.is_initial() && group.kind.is_entrypoint()
190 }) {
191 compilation
192 .build_chunk_graph_artifact
193 .chunk_graph
194 .connect_chunk_and_entry_module(new_chunk_ukey, m, *group);
195 }
196 }
197 }
198 }
199
200 Ok(None)
201}
202
203impl Plugin for RemoveDuplicateModulesPlugin {
204 fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
205 ctx
206 .compilation_hooks
207 .optimize_chunks
208 .tap(optimize_chunks::new(self));
209 Ok(())
210 }
211}