Skip to content

Commit c54eda4

Browse files
authored
[turbopack] Mark packages as side effect free when local analysis determines that they are. (#86398)
This implements some simple static analysis to determine if modules are side effect free based on their top level module evaluation items. The rules are simple * declarations ✅ * literals ✅ * property access ✅ (potentially controversal but consistent with other bundlers) * function calls ❌ (unless part of an builtin allowlist, e.g. `Object.keys`) imports are obviously the difficult part, for that reason we can evaluate a module to `SideEffectful` `SideEffectFree` or `ModuleEvaluationIsSideEffectFree`. A later step will leverage a graph analysis step to determine of transitive dependencies of ModuleEvaluationIsSideEffectFree modules are also side effect free and upgrade it. So for right now the impact is minor. There is also an experimental option to disable it in case it causes trouble this can help us debug I tested the performance on vercel site and as expected it isn't really observable with side effect analysis disabled ``` ~/projects/front/apps/vercel-site (main *)$ hyperfine -p 'rm -rf .next' -w 2 -r 20 'pnpm next build --turbopack --experimental-build-mode=compile' Benchmark 1: pnpm next build --turbopack --experimental-build-mode=compile Time (mean ± σ): 48.017 s ± 1.696 s [User: 319.651 s, System: 44.273 s] Range (min … max): 44.855 s … 50.600 s 20 runs ``` With it enabled. ``` ~/projects/front/apps/vercel-site (main *)$ hyperfine -p 'rm -rf .next' -w 2 -r 20 'pnpm next build --turbopack --experimental-build-mode=compile' Benchmark 1: pnpm next build --turbopack --experimental-build-mode=compile Time (mean ± σ): 48.038 s ± 2.337 s [User: 320.261 s, System: 45.434 s] Range (min … max): 44.156 s … 52.774 s 20 runs ``` so if there is a regression it is well within the noise.
1 parent b17b31f commit c54eda4

File tree

61 files changed

+2864
-14
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+2864
-14
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/next-core/src/next_client/context.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ pub async fn get_client_module_options_context(
326326
ecmascript: EcmascriptOptionsContext {
327327
enable_typeof_window_inlining: Some(TypeofWindow::Object),
328328
source_maps,
329+
infer_module_side_effects: *next_config.turbopack_infer_module_side_effects().await?,
329330
..Default::default()
330331
},
331332
css: CssOptionsContext {

crates/next-core/src/next_config.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,8 @@ pub struct ExperimentalConfig {
10551055
turbopack_remove_unused_imports: Option<bool>,
10561056
/// Defaults to false in development mode, true in production mode.
10571057
turbopack_remove_unused_exports: Option<bool>,
1058+
/// Enable local analysis to infer side effect free modules. Defaults to true.
1059+
turbopack_infer_module_side_effects: Option<bool>,
10581060
/// Devtool option for the segment explorer.
10591061
devtool_segment_explorer: Option<bool>,
10601062
}
@@ -2028,6 +2030,15 @@ impl NextConfig {
20282030
))
20292031
}
20302032

2033+
#[turbo_tasks::function]
2034+
pub fn turbopack_infer_module_side_effects(&self) -> Vc<bool> {
2035+
Vc::cell(
2036+
self.experimental
2037+
.turbopack_infer_module_side_effects
2038+
.unwrap_or(true),
2039+
)
2040+
}
2041+
20312042
#[turbo_tasks::function]
20322043
pub async fn module_ids(&self, mode: Vc<NextMode>) -> Result<Vc<ModuleIds>> {
20332044
Ok(match *mode.await? {

crates/next-core/src/next_server/context.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@ pub async fn get_server_module_options_context(
582582
import_externals: *next_config.import_externals().await?,
583583
ignore_dynamic_requests: true,
584584
source_maps,
585+
infer_module_side_effects: *next_config.turbopack_infer_module_side_effects().await?,
585586
..Default::default()
586587
},
587588
execution_context: Some(execution_context),

packages/next/src/server/config-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ export const experimentalSchema = {
310310
turbopackUseBuiltinBabel: z.boolean().optional(),
311311
turbopackUseBuiltinSass: z.boolean().optional(),
312312
turbopackModuleIds: z.enum(['named', 'deterministic']).optional(),
313+
turbopackInferModuleSideEffects: z.boolean().optional(),
313314
optimizePackageImports: z.array(z.string()).optional(),
314315
optimizeServerReact: z.boolean().optional(),
315316
clientTraceMetadata: z.array(z.string()).optional(),

packages/next/src/server/config-shared.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,15 @@ export interface ExperimentalConfig {
464464
*/
465465
turbopackRemoveUnusedExports?: boolean
466466

467+
/**
468+
* Enable local analysis to infer side effect free modules. When enabled, Turbopack will
469+
* analyze module code to determine if it has side effects. This can improve tree shaking
470+
* and bundle size at the cost of some additional analysis.
471+
*
472+
* Defaults to `true`.
473+
*/
474+
turbopackInferModuleSideEffects?: boolean
475+
467476
/**
468477
* Use the system-provided CA roots instead of bundled CA roots for external HTTPS requests
469478
* made by Turbopack. Currently this is only used for fetching data from Google Fonts.

turbopack/crates/turbopack-core/src/module.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
use bincode::{Decode, Encode};
2+
use serde::{Deserialize, Serialize};
13
use turbo_rcstr::RcStr;
2-
use turbo_tasks::{ResolvedVc, TaskInput, ValueToString, Vc};
4+
use turbo_tasks::{NonLocalValue, ResolvedVc, TaskInput, ValueToString, Vc, trace::TraceRawVcs};
35
use turbo_tasks_fs::glob::Glob;
46

57
use crate::{asset::Asset, ident::AssetIdent, reference::ModuleReferences, source::OptionSource};
@@ -11,6 +13,22 @@ pub enum StyleType {
1113
GlobalStyle,
1214
}
1315

16+
#[derive(
17+
Serialize, Deserialize, Hash, Eq, PartialEq, Debug, NonLocalValue, TraceRawVcs, Encode, Decode,
18+
)]
19+
pub enum ModuleSideEffects {
20+
/// Analysis determined that the module evaluation is side effect free
21+
/// the module may still be side effectful based on its imports.
22+
ModuleEvaluationIsSideEffectFree,
23+
/// Is known to be side effect free either due to static analysis or some kind of configuration.
24+
/// ```js
25+
/// "use turbopack no side effects"
26+
/// ```
27+
SideEffectFree,
28+
// Neither of the above, so we should assume it has side effects.
29+
SideEffectful,
30+
}
31+
1432
/// A module. This usually represents parsed source code, which has references
1533
/// to other modules.
1634
#[turbo_tasks::value_trait]
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
use anyhow::Result;
2+
use rustc_hash::{FxHashMap, FxHashSet};
3+
use turbo_tasks::{ResolvedVc, TryJoinIterExt, Vc};
4+
5+
use crate::{
6+
module::{Module, ModuleSideEffects},
7+
module_graph::{GraphTraversalAction, ModuleGraph, SingleModuleGraphWithBindingUsage},
8+
};
9+
10+
/// This lists all the modules that are side effect free
11+
/// This means they are either declared side effect free by some configuration or they have been
12+
/// determined to be side effect free via static analysis of the module evaluation and dependencies.
13+
#[turbo_tasks::value(transparent)]
14+
pub struct SideEffectFreeModules(FxHashSet<ResolvedVc<Box<dyn Module>>>);
15+
16+
/// Computes the set of side effect free modules in the module graph.
17+
///
18+
/// This leverages the module graph to compute if modules with `ModuleEvalutationIsSideEffectFree`
19+
/// status are actually side effectful or not.
20+
///
21+
/// A current limitation is that the module graph doesn't contain information on how 'late' imports
22+
/// are used. e.g. if there is a dynamic import with side effects in an event handler, we will mark
23+
/// the module as side effectful even though it isn't. To fix this the module graph would need to
24+
/// record more information about the nature of the edges.
25+
#[turbo_tasks::function]
26+
pub async fn compute_side_effect_free_module_info(
27+
graphs: ResolvedVc<ModuleGraph>,
28+
) -> Result<Vc<SideEffectFreeModules>> {
29+
// Layout segment optimization, we can individually compute the side effect free modules for
30+
// each graph.
31+
let mut result: Vc<SideEffectFreeModules> = Vc::cell(Default::default());
32+
let graphs = graphs.await?;
33+
for graph in graphs.iter_graphs() {
34+
result = compute_side_effect_free_module_info_single(graph, result);
35+
}
36+
Ok(result)
37+
}
38+
39+
#[turbo_tasks::function]
40+
async fn compute_side_effect_free_module_info_single(
41+
graph: SingleModuleGraphWithBindingUsage,
42+
parent_side_effect_free_modules: Vc<SideEffectFreeModules>,
43+
) -> Result<Vc<SideEffectFreeModules>> {
44+
let parent_side_effect_free_modules = parent_side_effect_free_modules.await?;
45+
let graph = graph.read().await?;
46+
47+
let module_side_effects = graph
48+
.enumerate_nodes()
49+
.map(async |(_, node)| {
50+
Ok(match node {
51+
super::SingleModuleGraphNode::Module(module) => {
52+
// This turbo task always has a cache hit since it is called when building the
53+
// module graph. we could consider moving this information
54+
// into to the module graph, but then changes would invalidate the whole graph.
55+
(*module, *module.side_effects().await?)
56+
}
57+
super::SingleModuleGraphNode::VisitedModule { idx: _, module } => (
58+
*module,
59+
if parent_side_effect_free_modules.contains(module) {
60+
ModuleSideEffects::SideEffectFree
61+
} else {
62+
ModuleSideEffects::SideEffectful
63+
},
64+
),
65+
})
66+
})
67+
.try_join()
68+
.await?
69+
.into_iter()
70+
.collect::<FxHashMap<_, _>>();
71+
72+
// Modules are categorized as side-effectful, locally side effect free and side effect free.
73+
// So we are really just interested in determining what modules that are locally side effect
74+
// free. logically we want to start at all such modules are determine if their transitive
75+
// dependencies are side effect free.
76+
77+
let mut locally_side_effect_free_modules_that_have_side_effects = FxHashSet::default();
78+
graph.traverse_edges_from_entries_dfs_reversed(
79+
// Start from all the side effectful nodes
80+
module_side_effects.iter().filter_map(|(m, e)| {
81+
if *e == ModuleSideEffects::SideEffectful {
82+
Some(*m)
83+
} else {
84+
None
85+
}
86+
}),
87+
&mut (),
88+
// child is a previously visited module that we know is side effectful
89+
// parent is a module that depends on it.
90+
|child, parent, _s| {
91+
Ok(if child.is_some() {
92+
match module_side_effects.get(&parent).unwrap() {
93+
ModuleSideEffects::SideEffectful | ModuleSideEffects::SideEffectFree => {
94+
// We have either already seen this or don't want to follow it
95+
GraphTraversalAction::Exclude
96+
}
97+
ModuleSideEffects::ModuleEvaluationIsSideEffectFree => {
98+
// this module is side effect free locally but depends on `child` which is
99+
// effectful so it too is effectful
100+
locally_side_effect_free_modules_that_have_side_effects.insert(parent);
101+
GraphTraversalAction::Continue
102+
}
103+
}
104+
} else {
105+
// entry point, we already determined it was effectful, keep going
106+
GraphTraversalAction::Continue
107+
})
108+
},
109+
|_, _, _| Ok(()),
110+
)?;
111+
let side_effect_free_modules = module_side_effects
112+
.into_iter()
113+
.filter_map(|(m, e)| match e {
114+
ModuleSideEffects::SideEffectful => None,
115+
ModuleSideEffects::SideEffectFree => Some(m),
116+
ModuleSideEffects::ModuleEvaluationIsSideEffectFree => {
117+
if locally_side_effect_free_modules_that_have_side_effects.contains(&m) {
118+
None
119+
} else {
120+
Some(m)
121+
}
122+
}
123+
})
124+
.chain(parent_side_effect_free_modules.iter().copied())
125+
.collect::<FxHashSet<_>>();
126+
127+
Ok(Vc::cell(side_effect_free_modules))
128+
}

turbopack/crates/turbopack-ecmascript/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ num-bigint = "0.4"
3535
num-traits = "0.2.15"
3636
once_cell = { workspace = true }
3737
parking_lot = { workspace = true }
38+
phf = { version = "0.11", features = ["macros"] }
3839
petgraph = { workspace = true }
3940
dashmap = { workspace = true }
4041
regex = { workspace = true }

turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub mod builtin;
4141
pub mod graph;
4242
pub mod imports;
4343
pub mod linker;
44+
pub mod side_effects;
4445
pub mod top_level_await;
4546
pub mod well_known;
4647

0 commit comments

Comments
 (0)