turbopack_core/module_graph/
export_usage.rs

1//! Intermediate tree shaking that uses global information but not good as the full tree shaking.
2
3use anyhow::Result;
4use auto_hash_map::AutoSet;
5use rustc_hash::{FxHashMap, FxHashSet};
6use turbo_rcstr::RcStr;
7use turbo_tasks::{ResolvedVc, Vc};
8
9use crate::{module::Module, module_graph::ModuleGraph, resolve::ExportUsage};
10
11#[turbo_tasks::function(operation)]
12pub async fn compute_export_usage_info(
13    graph: ResolvedVc<ModuleGraph>,
14) -> Result<Vc<ExportUsageInfo>> {
15    let mut used_exports = FxHashMap::<_, ModuleExportUsageInfo>::default();
16    let graph = graph.read_graphs().await?;
17    graph.traverse_all_edges_unordered(|(_, ref_data), target| {
18        let e = used_exports.entry(target.module).or_default();
19
20        e.add(&ref_data.export);
21
22        Ok(())
23    })?;
24
25    // Compute cycles and select modules to be 'circuit breakers'
26    // A circuit breaker module will need to eagerly export lazy getters for its exports to break an
27    // evaluation cycle all other modules can export values after defining them
28    let mut circuit_breakers = FxHashSet::default();
29    graph.traverse_cycles(
30        |e| e.chunking_type.is_parallel(),
31        |cycle| {
32            // To break cycles we need to ensure that no importing module can observe a
33            // partially populated exports object.
34
35            // We could compute this based on the module graph via a DFS from each entry point
36            // to the cycle.  Whatever node is hit first is an entry point to the cycle.
37            // (scope hoisting does something similar) and then we would only need to
38            // mark 'entry' modules (basically the targets of back edges in the export graph) as
39            // circuit breakers.  For now we just mark everything on the theory that cycles are
40            // rare.  For vercel-site on 8/22/2025 there were 106 cycles covering 800 modules
41            // (or 1.2% of all modules).  So with this analysis we could potentially drop 80% of
42            // the cycle breaker modules.
43            circuit_breakers.extend(cycle.iter().map(|n| n.module));
44        },
45    )?;
46
47    Ok(ExportUsageInfo {
48        used_exports,
49        circuit_breakers,
50    }
51    .cell())
52}
53
54#[turbo_tasks::value(transparent)]
55pub struct OptionExportUsageInfo(Option<ResolvedVc<ExportUsageInfo>>);
56
57#[turbo_tasks::value]
58pub struct ExportUsageInfo {
59    used_exports: FxHashMap<ResolvedVc<Box<dyn Module>>, ModuleExportUsageInfo>,
60    circuit_breakers: FxHashSet<ResolvedVc<Box<dyn Module>>>,
61}
62
63#[turbo_tasks::value(shared)]
64pub struct ModuleExportUsage {
65    pub export_usage: ResolvedVc<ModuleExportUsageInfo>,
66    // Whether this module exists in an import cycle and has been selected to break the cycle.
67    pub is_circuit_breaker: bool,
68}
69
70#[turbo_tasks::value_impl]
71impl ModuleExportUsage {
72    #[turbo_tasks::function]
73    pub async fn all() -> Result<Vc<Self>> {
74        Ok(Self {
75            export_usage: ModuleExportUsageInfo::all().to_resolved().await?,
76            is_circuit_breaker: true,
77        }
78        .cell())
79    }
80}
81
82impl ExportUsageInfo {
83    pub async fn used_exports(
84        &self,
85        module: ResolvedVc<Box<dyn Module>>,
86    ) -> Result<Vc<ModuleExportUsage>> {
87        let is_circuit_breaker = self.circuit_breakers.contains(&module);
88        let export_usage = if let Some(exports) = self.used_exports.get(&module) {
89            exports.clone().resolved_cell()
90        } else {
91            // We exclude template files from tree shaking because they are entrypoints to the
92            // module graph.
93            ModuleExportUsageInfo::all().to_resolved().await?
94        };
95        Ok(ModuleExportUsage {
96            export_usage,
97            is_circuit_breaker,
98        }
99        .cell())
100    }
101}
102
103#[turbo_tasks::value]
104#[derive(Default, Clone)]
105pub enum ModuleExportUsageInfo {
106    /// Only the side effects are needed, no exports is used.
107    #[default]
108    Evaluation,
109    Exports(AutoSet<RcStr>),
110    All,
111}
112
113#[turbo_tasks::value_impl]
114impl ModuleExportUsageInfo {
115    #[turbo_tasks::function]
116    pub fn all() -> Vc<Self> {
117        ModuleExportUsageInfo::All.cell()
118    }
119}
120
121impl ModuleExportUsageInfo {
122    fn add(&mut self, usage: &ExportUsage) {
123        match (&mut *self, usage) {
124            (Self::All, _) => {}
125            (_, ExportUsage::All) => {
126                *self = Self::All;
127            }
128            (Self::Evaluation, ExportUsage::Named(name)) => {
129                // Promote evaluation to something more specific
130                *self = Self::Exports(AutoSet::from_iter([name.clone()]));
131            }
132
133            (Self::Exports(l), ExportUsage::Named(r)) => {
134                // Merge exports
135                l.insert(r.clone());
136            }
137
138            (_, ExportUsage::Evaluation) => {
139                // Ignore evaluation
140            }
141        }
142    }
143
144    pub fn is_export_used(&self, export: &RcStr) -> bool {
145        match self {
146            Self::All => true,
147            Self::Evaluation => false,
148            Self::Exports(exports) => exports.contains(export),
149        }
150    }
151}