Skip to main content

rspack_plugin_remove_duplicate_modules/
lib.rs

1use 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  /*
64    sort chunks so that do max effort to find reusable chunk
65    eg. 3 entry
66    entry1: [main, foo, bar]
67    entry2: [foo, bar]
68    entry3: [bar]
69
70    the chunk map is
71    main:[entry1]
72    foo: [entry1, entry2]
73    bar: [entry1, entry2, entry3]
74
75    sorted
76    1. so bar gets split first,
77      found usable chunk entry3!
78    2. then split foo, found usable chunk entry2!
79
80    the result chunk is
81    main -> foo -> bar
82
83    the algorithm is easy and cannot cover all optimization possibilities, but
84    its performance is good and it works for most sceneries, if you have better
85    algorithm feel free to contribute, thanks
86  */
87  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    // split chunks from original chunks and create new chunk
96    let new_chunk_ukey = if let Some(chunk) = find_reusable_chunk(compilation, &chunks, &modules) {
97      // we can use this chunk directly
98      // all modules are into existing chunk, the chunkMap needs update
99
100      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}