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            // TODO this would be wrong with emitted CSS modules
94            None,
95            &mut (),
96            |parent_info, module, _| {
97                if let Some((_, ModuleBatchesGraphEdge { ty, .. })) = parent_info
98                    && !ty.is_parallel()
99                {
100                    return Ok(GraphTraversalAction::Exclude);
101                }
102                if visited.insert(module) {
103                    Ok(GraphTraversalAction::Continue)
104                } else {
105                    Ok(GraphTraversalAction::Exclude)
106                }
107            },
108            |parent_info, item, _| {
109                if let Some((_, ModuleBatchesGraphEdge { ty, .. })) = parent_info
110                    && !ty.is_parallel()
111                {
112                    return;
113                }
114                items_in_postorder.insert(*item);
115            },
116        )?;
117
118        let mut styles = FxIndexSet::default();
119        let mut handle_module = async |module| {
120            match module_info_map.entry(module) {
121                Entry::Occupied(mut e) => {
122                    if let Some(info) = e.get_mut() {
123                        info.chunk_group_indices.insert(idx, styles.len());
124                        info.index_sum += styles.len();
125                        styles.insert(module);
126                    }
127                }
128                Entry::Vacant(e) => {
129                    if let Some(style_module) =
130                        ResolvedVc::try_sidecast::<Box<dyn StyleModule>>(module)
131                    {
132                        let style_type = *style_module.style_type().await?;
133                        let mut info =
134                            ModuleInfo::new(style_type, module.ident().to_string().owned().await?);
135                        info.chunk_group_indices.insert(idx, styles.len());
136                        info.index_sum += styles.len();
137                        styles.insert(module);
138                        e.insert(Some(info));
139                    } else {
140                        e.insert(None);
141                    }
142                }
143            }
144            anyhow::Ok(())
145        };
146
147        for item in items_in_postorder {
148            match item {
149                ModuleOrBatch::Batch(batch) => {
150                    for &module in &batch.await?.modules {
151                        handle_module(module).await?;
152                    }
153                }
154                ModuleOrBatch::Module(module) => {
155                    if let Some(chunkable_module) = ResolvedVc::try_downcast(module) {
156                        handle_module(chunkable_module).await?;
157                    }
158                }
159                ModuleOrBatch::None(_) => {}
160            }
161        }
162
163        if !styles.is_empty() {
164            chunk_group_state.push(ChunkGroupState {
165                requests: styles.len(),
166                styles,
167            });
168            idx += 1;
169        }
170    }
171
172    module_info_map.retain(|_, info| info.is_some());
173
174    module_info_map.sort_by(|_, a, _, b| {
175        let a = a.as_ref().unwrap();
176        let b = b.as_ref().unwrap();
177        a.index_sum
178            .cmp(&b.index_sum)
179            .then_with(|| a.ident.cmp(&b.ident))
180    });
181
182    // Compute the chunk item and size of each module
183    let chunk_item_and_sizes = module_info_map
184        .keys()
185        .map(async |&module| {
186            let chunk_item = attach_async_info_to_chunkable_module(
187                module,
188                async_module_info,
189                module_graph,
190                chunking_context,
191            )
192            .await?;
193            let size = *chunk_item
194                .chunk_type
195                .chunk_item_size(chunking_context, *chunk_item.chunk_item, None)
196                .await?;
197            Ok((chunk_item, size))
198        })
199        .try_join()
200        .await?;
201    module_info_map
202        .iter_mut()
203        .zip(chunk_item_and_sizes)
204        .for_each(|((_, info), (chunk_item, size))| {
205            let info = info.as_mut().unwrap();
206            info.size = size;
207            info.chunk_item = Some(chunk_item);
208        });
209
210    Ok(StyleCollection {
211        module_info_map,
212        chunk_group_state,
213    })
214}
215
216pub async fn compute_style_groups(
217    module_graph: Vc<ModuleGraph>,
218    chunking_context: Vc<Box<dyn ChunkingContext>>,
219    config: &StyleGroupsConfig,
220) -> Result<Vc<StyleGroups>> {
221    let StyleCollection {
222        module_info_map,
223        mut chunk_group_state,
224    } = collect_style_modules_per_chunk_group(module_graph, chunking_context).await?;
225
226    // Compute the dependents of each module
227    let mut module_dependents: FxHashMap<_, Vec<_>> = FxHashMap::default();
228    for (&module, info) in &module_info_map {
229        let info = info.as_ref().unwrap();
230        // Find the shortest chunk group as it's most efficient to iterate
231        let (&idx, &start_pos) = info
232            .chunk_group_indices
233            .iter()
234            .min_by_key(|&(&idx, _)| chunk_group_state[idx].styles.len())
235            .unwrap();
236        let potential_dependents = &chunk_group_state[idx].styles[start_pos + 1..];
237
238        let dependents = potential_dependents
239            .iter()
240            .copied()
241            .filter(|dependent| {
242                let dependent_info = module_info_map.get(dependent).unwrap();
243                let dependent_info = dependent_info.as_ref().unwrap();
244
245                // module is a dependency of dependent when it's included in all chunk groups of
246                // dependent with an index lower than the index of the dependent
247                info.chunk_group_indices.len() >= dependent_info.chunk_group_indices.len()
248                    && dependent_info
249                        .chunk_group_indices
250                        .iter()
251                        .all(|(idx, &dependent_pos)| {
252                            info.chunk_group_indices
253                                .get(idx)
254                                .is_some_and(|&module_pos| module_pos < dependent_pos)
255                        })
256            })
257            .collect::<Vec<_>>();
258
259        if !dependents.is_empty() {
260            module_dependents.insert(module, dependents);
261        }
262    }
263
264    let mut ordered_modules_with_state = module_info_map
265        .keys()
266        .copied()
267        .map(|m| (m, false))
268        .collect::<FxIndexMap<_, _>>();
269
270    let mut shared_chunk_items = FxIndexMap::default();
271    for i in 0..ordered_modules_with_state.len() {
272        let (&module, processed) = ordered_modules_with_state.get_index_mut(i).unwrap();
273        if *processed {
274            continue;
275        }
276        *processed = true;
277
278        let info = module_info_map.get(&module).unwrap().as_ref().unwrap();
279        let mut global_mode = info.style_type == StyleType::GlobalStyle;
280
281        // The current position of processing in all selected chunk groups
282        let mut all_chunk_states = info.chunk_group_indices.clone();
283
284        // The list of modules and chunk items that go into the new chunk
285        let mut new_chunk_modules = [module].into_iter().collect::<FxHashSet<_>>();
286        let mut new_chunk_items = vec![info.chunk_item.unwrap()];
287
288        // The current size of the new chunk
289        let mut current_size = info.size;
290
291        // A pool of potential modules where the next module is selected from.
292        // It's filled from the next module of the selected modules in every chunk group.
293        let mut potential_next_modules = all_chunk_states
294            .iter()
295            .filter_map(|(&idx, pos)| {
296                let following_styles = &chunk_group_state[idx].styles[pos + 1..];
297                let i = following_styles
298                    .iter()
299                    .position(|m| !*ordered_modules_with_state.get(m).unwrap());
300                i.map(|i| following_styles[i])
301            })
302            .collect::<FxHashSet<_>>();
303
304        // Try to add modules to the chunk until a break condition is met
305        'outer: loop {
306            // We try to select a module that reduces request count and
307            // has the highest number of requests
308            let mut ordered_potential_next_modules = potential_next_modules
309                .iter()
310                .copied()
311                .map(|module| {
312                    let info = module_info_map.get(&module).unwrap().as_ref().unwrap();
313                    let requests = info
314                        .chunk_group_indices
315                        .keys()
316                        .filter(|idx| all_chunk_states.contains_key(idx))
317                        .map(|&idx| chunk_group_state[idx].requests)
318                        .max()
319                        .unwrap();
320                    (module, info, requests)
321                })
322                .collect::<Vec<_>>();
323            ordered_potential_next_modules
324                .sort_by_key(|(_, info, requests)| (Reverse(*requests), &info.ident));
325
326            // Try every potential module
327            for (module, info, _) in ordered_potential_next_modules {
328                if current_size + info.size > config.max_chunk_size {
329                    // Chunk would be too large
330                    continue;
331                }
332                // In loose mode we only check if the dependencies are not violated
333                if let Some(dependents) = module_dependents.get(&module)
334                    && dependents.iter().any(|m| new_chunk_modules.contains(m))
335                {
336                    // A dependent of the module is already in the chunk, which would violate
337                    // the order
338                    continue;
339                }
340
341                // Global CSS must not leak into unrelated chunks
342                let is_global = info.style_type == StyleType::GlobalStyle;
343                if is_global
344                    && global_mode
345                    && all_chunk_states.len() != info.chunk_group_indices.len()
346                {
347                    // Fast check: chunk groups need to be identical
348                    continue;
349                }
350                if global_mode
351                    && info
352                        .chunk_group_indices
353                        .keys()
354                        .any(|idx| !all_chunk_states.contains_key(idx))
355                {
356                    // Global CSS in new_chunk_items would leak into new chunk_group
357                    continue;
358                }
359                if is_global
360                    && all_chunk_states
361                        .keys()
362                        .any(|idx| !info.chunk_group_indices.contains_key(idx))
363                {
364                    // Global CSS would leak into existing chunk_group
365                    continue;
366                }
367                potential_next_modules.remove(&module);
368                current_size += info.size;
369                if is_global {
370                    global_mode = true;
371                }
372                for &idx in info.chunk_group_indices.keys() {
373                    if all_chunk_states.contains_key(&idx) {
374                        // This reduces the request count of the chunk group
375                        chunk_group_state[idx].requests -= 1;
376                    }
377                    let pos = chunk_group_state[idx].styles.get_index_of(&module).unwrap();
378                    all_chunk_states.insert(idx, pos);
379                    let following_styles = &chunk_group_state[idx].styles[pos + 1..];
380                    if let Some(i) = following_styles.iter().position(|m| {
381                        !*ordered_modules_with_state.get(m).unwrap()
382                            && !new_chunk_modules.contains(m)
383                    }) {
384                        let module = following_styles[i];
385                        potential_next_modules.insert(module);
386                    }
387                }
388
389                new_chunk_items.push(info.chunk_item.unwrap());
390                new_chunk_modules.insert(module);
391                *ordered_modules_with_state.get_mut(&module).unwrap() = true;
392                continue 'outer;
393            }
394            break;
395        }
396
397        if new_chunk_items.len() > 1 {
398            let style_group = ChunkItemBatchWithAsyncModuleInfo::new(new_chunk_items.clone())
399                .to_resolved()
400                .await?;
401            for chunk_item in new_chunk_items {
402                shared_chunk_items.insert(
403                    chunk_item,
404                    StyleItemInfo {
405                        order: None,
406                        batch: Some(style_group),
407                    },
408                );
409            }
410        }
411    }
412
413    Ok(StyleGroups { shared_chunk_items }.cell())
414}