Skip to main content

turbopack_core/module_graph/style_groups_loose/
mod.rs

1use std::cmp::Reverse;
2
3use anyhow::Result;
4use indexmap::map::Entry;
5use rustc_hash::{FxHashMap, FxHashSet};
6use turbo_rcstr::RcStr;
7use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryJoinIterExt, ValueToString, Vc};
8
9use crate::{
10    chunk::{
11        ChunkItemBatchWithAsyncModuleInfo, ChunkItemWithAsyncModuleInfo, ChunkType,
12        ChunkableModule, ChunkingContext, chunk_item_batch::attach_async_info_to_chunkable_module,
13    },
14    module::{Module, StyleModule, StyleType},
15    module_graph::{
16        GraphTraversalAction, ModuleGraph,
17        module_batch::ModuleOrBatch,
18        module_batches::ModuleBatchesGraphEdge,
19        style_groups::{StyleGroups, StyleGroupsConfig, StyleItemInfo},
20    },
21};
22
23#[derive(Debug)]
24struct ModuleInfo {
25    style_type: StyleType,
26    ident: RcStr,
27    chunk_group_indices: FxHashMap<usize, usize>,
28    index_sum: usize,
29    size: usize,
30    chunk_item: Option<ChunkItemWithAsyncModuleInfo>,
31}
32
33impl ModuleInfo {
34    fn new(style_type: StyleType, ident: RcStr) -> Self {
35        Self {
36            style_type,
37            ident,
38            chunk_group_indices: Default::default(),
39            index_sum: 0,
40            size: 0,
41            chunk_item: None,
42        }
43    }
44}
45
46struct ChunkGroupState {
47    styles: FxIndexSet<ResolvedVc<Box<dyn ChunkableModule>>>,
48    /// Number of distinct chunks this chunk group still needs to load. The legacy algorithm
49    /// decrements this as it merges items into shared chunks.
50    requests: usize,
51}
52
53/// Per-chunk-group style module collection plus per-module metadata. Internal to the legacy
54/// algorithm.
55struct StyleCollection {
56    /// Per-module info, keyed by chunkable module. After collection, every value is `Some`
57    /// (vacant entries used while traversing have been dropped). The map is sorted by
58    /// `(index_sum, ident)` so insertion order is deterministic.
59    module_info_map: FxIndexMap<ResolvedVc<Box<dyn ChunkableModule>>, Option<ModuleInfo>>,
60    /// Per-chunk-group state. Indexed by the same `idx` stored in
61    /// `ModuleInfo::chunk_group_indices`.
62    chunk_group_state: Vec<ChunkGroupState>,
63}
64
65/// Walk every chunk group in `module_graph` post-order, collecting:
66///  * the ordered list of CSS modules each chunk group loads,
67///  * per-module metadata (style type, ident, size, chunk item, and per-group position).
68async fn collect_style_modules_per_chunk_group(
69    module_graph: Vc<ModuleGraph>,
70    chunking_context: Vc<Box<dyn ChunkingContext>>,
71) -> Result<StyleCollection> {
72    let chunk_group_info = module_graph.chunk_group_info().await?;
73    let batches_graph = module_graph
74        .module_batches(chunking_context.batching_config())
75        .await?;
76    let async_module_info = module_graph.async_module_info();
77    let mut module_info_map: FxIndexMap<ResolvedVc<Box<dyn ChunkableModule>>, Option<ModuleInfo>> =
78        FxIndexMap::default();
79
80    // Compute the style modules in each chunk group
81    let mut chunk_group_state: Vec<ChunkGroupState> = Vec::new();
82    let mut idx = 0;
83    for (i, chunk_group) in chunk_group_info.chunk_groups.iter().enumerate() {
84        let ordered_entries = batches_graph.get_ordered_entries(&chunk_group_info, i);
85        let mut entries = Vec::with_capacity(chunk_group.entries_count());
86        for entry in ordered_entries {
87            entries.push(batches_graph.get_entry_index(entry).await?);
88        }
89        let mut visited = FxHashSet::default();
90        let mut items_in_postorder = FxIndexSet::default();
91        batches_graph.traverse_edges_from_entries_dfs(
92            entries.iter().copied(),
93            &mut (),
94            |parent_info, module, _| {
95                if let Some((_, ModuleBatchesGraphEdge { ty, .. })) = parent_info
96                    && !ty.is_parallel()
97                {
98                    return Ok(GraphTraversalAction::Exclude);
99                }
100                if visited.insert(module) {
101                    Ok(GraphTraversalAction::Continue)
102                } else {
103                    Ok(GraphTraversalAction::Exclude)
104                }
105            },
106            |parent_info, item, _| {
107                if let Some((_, ModuleBatchesGraphEdge { ty, .. })) = parent_info
108                    && !ty.is_parallel()
109                {
110                    return;
111                }
112                items_in_postorder.insert(*item);
113            },
114        )?;
115
116        let mut styles = FxIndexSet::default();
117        let mut handle_module = async |module| {
118            match module_info_map.entry(module) {
119                Entry::Occupied(mut e) => {
120                    if let Some(info) = e.get_mut() {
121                        info.chunk_group_indices.insert(idx, styles.len());
122                        info.index_sum += styles.len();
123                        styles.insert(module);
124                    }
125                }
126                Entry::Vacant(e) => {
127                    if let Some(style_module) =
128                        ResolvedVc::try_sidecast::<Box<dyn StyleModule>>(module)
129                    {
130                        let style_type = *style_module.style_type().await?;
131                        let mut info =
132                            ModuleInfo::new(style_type, module.ident().to_string().owned().await?);
133                        info.chunk_group_indices.insert(idx, styles.len());
134                        info.index_sum += styles.len();
135                        styles.insert(module);
136                        e.insert(Some(info));
137                    } else {
138                        e.insert(None);
139                    }
140                }
141            }
142            anyhow::Ok(())
143        };
144
145        for item in items_in_postorder {
146            match item {
147                ModuleOrBatch::Batch(batch) => {
148                    for &module in &batch.await?.modules {
149                        handle_module(module).await?;
150                    }
151                }
152                ModuleOrBatch::Module(module) => {
153                    if let Some(chunkable_module) = ResolvedVc::try_downcast(module) {
154                        handle_module(chunkable_module).await?;
155                    }
156                }
157                ModuleOrBatch::None(_) => {}
158            }
159        }
160
161        if !styles.is_empty() {
162            chunk_group_state.push(ChunkGroupState {
163                requests: styles.len(),
164                styles,
165            });
166            idx += 1;
167        }
168    }
169
170    module_info_map.retain(|_, info| info.is_some());
171
172    module_info_map.sort_by(|_, a, _, b| {
173        let a = a.as_ref().unwrap();
174        let b = b.as_ref().unwrap();
175        a.index_sum
176            .cmp(&b.index_sum)
177            .then_with(|| a.ident.cmp(&b.ident))
178    });
179
180    // Compute the chunk item and size of each module
181    let chunk_item_and_sizes = module_info_map
182        .keys()
183        .map(async |&module| {
184            let chunk_item = attach_async_info_to_chunkable_module(
185                module,
186                async_module_info,
187                module_graph,
188                chunking_context,
189            )
190            .await?;
191            let size = *chunk_item
192                .chunk_type
193                .chunk_item_size(chunking_context, *chunk_item.chunk_item, None)
194                .await?;
195            Ok((chunk_item, size))
196        })
197        .try_join()
198        .await?;
199    module_info_map
200        .iter_mut()
201        .zip(chunk_item_and_sizes)
202        .for_each(|((_, info), (chunk_item, size))| {
203            let info = info.as_mut().unwrap();
204            info.size = size;
205            info.chunk_item = Some(chunk_item);
206        });
207
208    Ok(StyleCollection {
209        module_info_map,
210        chunk_group_state,
211    })
212}
213
214pub async fn compute_style_groups(
215    module_graph: Vc<ModuleGraph>,
216    chunking_context: Vc<Box<dyn ChunkingContext>>,
217    config: &StyleGroupsConfig,
218) -> Result<Vc<StyleGroups>> {
219    let StyleCollection {
220        module_info_map,
221        mut chunk_group_state,
222    } = collect_style_modules_per_chunk_group(module_graph, chunking_context).await?;
223
224    // Compute the dependents of each module
225    let mut module_dependents: FxHashMap<_, Vec<_>> = FxHashMap::default();
226    for (&module, info) in &module_info_map {
227        let info = info.as_ref().unwrap();
228        // Find the shortest chunk group as it's most efficient to iterate
229        let (&idx, &start_pos) = info
230            .chunk_group_indices
231            .iter()
232            .min_by_key(|&(&idx, _)| chunk_group_state[idx].styles.len())
233            .unwrap();
234        let potential_dependents = &chunk_group_state[idx].styles[start_pos + 1..];
235
236        let dependents = potential_dependents
237            .iter()
238            .copied()
239            .filter(|dependent| {
240                let dependent_info = module_info_map.get(dependent).unwrap();
241                let dependent_info = dependent_info.as_ref().unwrap();
242
243                // module is a dependency of dependent when it's included in all chunk groups of
244                // dependent with an index lower than the index of the dependent
245                info.chunk_group_indices.len() >= dependent_info.chunk_group_indices.len()
246                    && dependent_info
247                        .chunk_group_indices
248                        .iter()
249                        .all(|(idx, &dependent_pos)| {
250                            info.chunk_group_indices
251                                .get(idx)
252                                .is_some_and(|&module_pos| module_pos < dependent_pos)
253                        })
254            })
255            .collect::<Vec<_>>();
256
257        if !dependents.is_empty() {
258            module_dependents.insert(module, dependents);
259        }
260    }
261
262    let mut ordered_modules_with_state = module_info_map
263        .keys()
264        .copied()
265        .map(|m| (m, false))
266        .collect::<FxIndexMap<_, _>>();
267
268    let mut shared_chunk_items = FxIndexMap::default();
269    for i in 0..ordered_modules_with_state.len() {
270        let (&module, processed) = ordered_modules_with_state.get_index_mut(i).unwrap();
271        if *processed {
272            continue;
273        }
274        *processed = true;
275
276        let info = module_info_map.get(&module).unwrap().as_ref().unwrap();
277        let mut global_mode = info.style_type == StyleType::GlobalStyle;
278
279        // The current position of processing in all selected chunk groups
280        let mut all_chunk_states = info.chunk_group_indices.clone();
281
282        // The list of modules and chunk items that go into the new chunk
283        let mut new_chunk_modules = [module].into_iter().collect::<FxHashSet<_>>();
284        let mut new_chunk_items = vec![info.chunk_item.unwrap()];
285
286        // The current size of the new chunk
287        let mut current_size = info.size;
288
289        // A pool of potential modules where the next module is selected from.
290        // It's filled from the next module of the selected modules in every chunk group.
291        let mut potential_next_modules = all_chunk_states
292            .iter()
293            .filter_map(|(&idx, pos)| {
294                let following_styles = &chunk_group_state[idx].styles[pos + 1..];
295                let i = following_styles
296                    .iter()
297                    .position(|m| !*ordered_modules_with_state.get(m).unwrap());
298                i.map(|i| following_styles[i])
299            })
300            .collect::<FxHashSet<_>>();
301
302        // Try to add modules to the chunk until a break condition is met
303        'outer: loop {
304            // We try to select a module that reduces request count and
305            // has the highest number of requests
306            let mut ordered_potential_next_modules = potential_next_modules
307                .iter()
308                .copied()
309                .map(|module| {
310                    let info = module_info_map.get(&module).unwrap().as_ref().unwrap();
311                    let requests = info
312                        .chunk_group_indices
313                        .keys()
314                        .filter(|idx| all_chunk_states.contains_key(idx))
315                        .map(|&idx| chunk_group_state[idx].requests)
316                        .max()
317                        .unwrap();
318                    (module, info, requests)
319                })
320                .collect::<Vec<_>>();
321            ordered_potential_next_modules
322                .sort_by_key(|(_, info, requests)| (Reverse(*requests), &info.ident));
323
324            // Try every potential module
325            for (module, info, _) in ordered_potential_next_modules {
326                if current_size + info.size > config.max_chunk_size {
327                    // Chunk would be too large
328                    continue;
329                }
330                // In loose mode we only check if the dependencies are not violated
331                if let Some(dependents) = module_dependents.get(&module)
332                    && dependents.iter().any(|m| new_chunk_modules.contains(m))
333                {
334                    // A dependent of the module is already in the chunk, which would violate
335                    // the order
336                    continue;
337                }
338
339                // Global CSS must not leak into unrelated chunks
340                let is_global = info.style_type == StyleType::GlobalStyle;
341                if is_global
342                    && global_mode
343                    && all_chunk_states.len() != info.chunk_group_indices.len()
344                {
345                    // Fast check: chunk groups need to be identical
346                    continue;
347                }
348                if global_mode
349                    && info
350                        .chunk_group_indices
351                        .keys()
352                        .any(|idx| !all_chunk_states.contains_key(idx))
353                {
354                    // Global CSS in new_chunk_items would leak into new chunk_group
355                    continue;
356                }
357                if is_global
358                    && all_chunk_states
359                        .keys()
360                        .any(|idx| !info.chunk_group_indices.contains_key(idx))
361                {
362                    // Global CSS would leak into existing chunk_group
363                    continue;
364                }
365                potential_next_modules.remove(&module);
366                current_size += info.size;
367                if is_global {
368                    global_mode = true;
369                }
370                for &idx in info.chunk_group_indices.keys() {
371                    if all_chunk_states.contains_key(&idx) {
372                        // This reduces the request count of the chunk group
373                        chunk_group_state[idx].requests -= 1;
374                    }
375                    let pos = chunk_group_state[idx].styles.get_index_of(&module).unwrap();
376                    all_chunk_states.insert(idx, pos);
377                    let following_styles = &chunk_group_state[idx].styles[pos + 1..];
378                    if let Some(i) = following_styles.iter().position(|m| {
379                        !*ordered_modules_with_state.get(m).unwrap()
380                            && !new_chunk_modules.contains(m)
381                    }) {
382                        let module = following_styles[i];
383                        potential_next_modules.insert(module);
384                    }
385                }
386
387                new_chunk_items.push(info.chunk_item.unwrap());
388                new_chunk_modules.insert(module);
389                *ordered_modules_with_state.get_mut(&module).unwrap() = true;
390                continue 'outer;
391            }
392            break;
393        }
394
395        if new_chunk_items.len() > 1 {
396            let style_group = ChunkItemBatchWithAsyncModuleInfo::new(new_chunk_items.clone())
397                .to_resolved()
398                .await?;
399            for chunk_item in new_chunk_items {
400                shared_chunk_items.insert(
401                    chunk_item,
402                    StyleItemInfo {
403                        order: None,
404                        batch: Some(style_group),
405                    },
406                );
407            }
408        }
409    }
410
411    Ok(StyleGroups { shared_chunk_items }.cell())
412}