diff --git a/crates/oxc_minifier/src/peephole/inline.rs b/crates/oxc_minifier/src/peephole/inline.rs index d901ca19d34f0..a2e1871da6ea4 100644 --- a/crates/oxc_minifier/src/peephole/inline.rs +++ b/crates/oxc_minifier/src/peephole/inline.rs @@ -1,6 +1,6 @@ use oxc_ast::ast::*; use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ConstantValue}; -use oxc_semantic::SymbolId; +use oxc_semantic::{ScopeFlags, ScopeId, SymbolId}; use oxc_span::GetSpan; use oxc_traverse::Ancestor; use rustc_hash::FxHashSet; @@ -83,13 +83,12 @@ impl<'a> PeepholeOptimizations { write_references_count += 1; } } - let scope_id = ctx.scoping().symbol_scope_id(symbol_id); let value = SymbolInformation { value: SymbolValue::default(), exported: exported_values.contains(&symbol_id), read_references_count, write_references_count, - scope_id, + scope_id: None, }; ctx.state.symbol_values.init_value(symbol_id, value); } @@ -98,15 +97,25 @@ impl<'a> PeepholeOptimizations { pub fn init_symbol_value(decl: &VariableDeclarator<'a>, ctx: &mut Ctx<'a, '_>) { let BindingPatternKind::BindingIdentifier(ident) = &decl.id.kind else { return }; let symbol_id = ident.symbol_id(); - if decl.kind.is_var() || Self::is_for_statement_init(ctx) { - // - Skip constant value inlining for `var` declarations, due to TDZ problems. - // - Set None for for statement initializers as the value of these are set by the for statement. + // Set None for for statement initializers as the value of these are set by the for statement. + if Self::is_for_statement_init(ctx) { return; } - let value = - decl.init.as_ref().map_or(Some(ConstantValue::Undefined), |e| e.evaluate_value(ctx)); - ctx.state.symbol_values.set_constant_value(symbol_id, value); + let init = decl.init.as_ref(); + let symbol_value = if let Some(value) = + init.map_or(Some(ConstantValue::Undefined), |e| e.evaluate_value(ctx)) + { + if decl.kind.is_var() { + SymbolValue::ScopedPrimitive(value) + } else { + SymbolValue::Primitive(value) + } + } else { + return; + }; + let current_scope_id = ctx.current_scope_id(); + ctx.state.symbol_values.set_value(symbol_id, symbol_value, current_scope_id); } fn is_for_statement_init(ctx: &Ctx<'a, '_>) -> bool { @@ -124,19 +133,53 @@ impl<'a> PeepholeOptimizations { if symbol_value.write_references_count > 0 { return; } - let SymbolValue::Primitive(cv) = &symbol_value.value else { return }; - if symbol_value.read_references_count == 1 - || match cv { - ConstantValue::Number(n) => n.fract() == 0.0 && *n >= -99.0 && *n <= 999.0, - ConstantValue::BigInt(_) => false, - ConstantValue::String(s) => s.len() <= 3, - ConstantValue::Boolean(_) | ConstantValue::Undefined | ConstantValue::Null => true, + match &symbol_value.value { + SymbolValue::Primitive(cv) => { + if symbol_value.read_references_count == 1 + || Self::can_inline_constant_multiple_times(cv) + { + *expr = ctx.value_to_expr(expr.span(), cv.clone()); + ctx.state.changed = true; + } } - { - *expr = ctx.value_to_expr(expr.span(), cv.clone()); - ctx.state.changed = true; + SymbolValue::ScopedPrimitive(cv) => { + if (symbol_value.read_references_count == 1 + || Self::can_inline_constant_multiple_times(cv)) + && symbol_value.scope_id.is_some_and(|declared_scope_id| { + Self::is_referenced_in_same_hoist_scope(declared_scope_id, ctx) + }) + { + *expr = ctx.value_to_expr(expr.span(), cv.clone()); + ctx.state.changed = true; + } + } + SymbolValue::Unknown => {} + } + } + + fn can_inline_constant_multiple_times(cv: &ConstantValue<'_>) -> bool { + match cv { + ConstantValue::Number(n) => n.fract() == 0.0 && *n >= -99.0 && *n <= 999.0, + ConstantValue::BigInt(_) => false, + ConstantValue::String(s) => s.len() <= 3, + ConstantValue::Boolean(_) | ConstantValue::Undefined | ConstantValue::Null => true, } } + + fn is_referenced_in_same_hoist_scope(declared_scope_id: ScopeId, ctx: &Ctx<'a, '_>) -> bool { + ctx.scoping() + .scope_ancestors(ctx.current_scope_id()) + .find_map(|scope_id| { + if declared_scope_id == scope_id { + return Some(true); + } + if ctx.scoping().scope_flags(scope_id).contains(ScopeFlags::Var) { + return Some(false); + } + None + }) + .unwrap_or_default() + } } #[cfg(test)] diff --git a/crates/oxc_minifier/src/symbol_value.rs b/crates/oxc_minifier/src/symbol_value.rs index 2a521d5f5fbdb..885e10a337e6e 100644 --- a/crates/oxc_minifier/src/symbol_value.rs +++ b/crates/oxc_minifier/src/symbol_value.rs @@ -5,8 +5,11 @@ use oxc_syntax::{scope::ScopeId, symbol::SymbolId}; #[derive(Debug, Default)] pub enum SymbolValue<'a> { - /// Initialized primitive constant value evaluated from expressions. + /// Initialized primitive constant value. Primitive(ConstantValue<'a>), + /// Initialized primitive value. + /// This can be inlined within the same scope after the variable is declared. + ScopedPrimitive(ConstantValue<'a>), #[default] Unknown, } @@ -27,8 +30,7 @@ pub struct SymbolInformation<'a> { pub read_references_count: u32, pub write_references_count: u32, - #[expect(unused)] - pub scope_id: ScopeId, + pub scope_id: Option, } #[derive(Debug, Default)] @@ -45,15 +47,15 @@ impl<'a> SymbolInformationMap<'a> { self.values.insert(symbol_id, symbol_value); } - pub fn set_constant_value( + pub fn set_value( &mut self, symbol_id: SymbolId, - symbol_value: Option>, + symbol_value: SymbolValue<'a>, + scope_id: ScopeId, ) { let info = self.values.get_mut(&symbol_id).expect("symbol value must exist"); - if let Some(constant) = symbol_value { - info.value = SymbolValue::Primitive(constant); - } + info.value = symbol_value; + info.scope_id = Some(scope_id); } pub fn get_symbol_value(&self, symbol_id: SymbolId) -> Option<&SymbolInformation<'a>> { diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 1758968bed3fb..9f73527dc712e 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -1,27 +1,27 @@ | Oxc | ESBuild | Oxc | ESBuild | Original | minified | minified | gzip | gzip | Iterations | File ------------------------------------------------------------------------------------- -72.14 kB | 23.21 kB | 23.70 kB | 8.38 kB | 8.54 kB | 2 | react.development.js +72.14 kB | 23.14 kB | 23.70 kB | 8.32 kB | 8.54 kB | 2 | react.development.js -173.90 kB | 59.44 kB | 59.82 kB | 19.16 kB | 19.33 kB | 2 | moment.js +173.90 kB | 59.33 kB | 59.82 kB | 19.08 kB | 19.33 kB | 2 | moment.js -287.63 kB | 89.28 kB | 90.07 kB | 30.94 kB | 31.95 kB | 2 | jquery.js +287.63 kB | 89.28 kB | 90.07 kB | 30.93 kB | 31.95 kB | 2 | jquery.js -342.15 kB | 117.01 kB | 118.14 kB | 43.19 kB | 44.37 kB | 2 | vue.js +342.15 kB | 116.94 kB | 118.14 kB | 43.14 kB | 44.37 kB | 2 | vue.js -544.10 kB | 71.18 kB | 72.48 kB | 25.85 kB | 26.20 kB | 2 | lodash.js +544.10 kB | 71.89 kB | 72.48 kB | 25.48 kB | 26.20 kB | 2 | lodash.js -555.77 kB | 270.78 kB | 270.13 kB | 88.19 kB | 90.80 kB | 2 | d3.js +555.77 kB | 270.62 kB | 270.13 kB | 88.13 kB | 90.80 kB | 2 | d3.js -1.01 MB | 439.58 kB | 458.89 kB | 122.15 kB | 126.71 kB | 2 | bundle.min.js +1.01 MB | 439.47 kB | 458.89 kB | 122.10 kB | 126.71 kB | 4 | bundle.min.js -1.25 MB | 645.63 kB | 646.76 kB | 159.54 kB | 163.73 kB | 2 | three.js +1.25 MB | 644.77 kB | 646.76 kB | 158.99 kB | 163.73 kB | 2 | three.js -2.14 MB | 713.54 kB | 724.14 kB | 161.00 kB | 181.07 kB | 2 | victory.js +2.14 MB | 713.33 kB | 724.14 kB | 160.72 kB | 181.07 kB | 2 | victory.js -3.20 MB | 1.00 MB | 1.01 MB | 323.12 kB | 331.56 kB | 3 | echarts.js +3.20 MB | 1.00 MB | 1.01 MB | 322.70 kB | 331.56 kB | 3 | echarts.js -6.69 MB | 2.22 MB | 2.31 MB | 459.28 kB | 488.28 kB | 4 | antd.js +6.69 MB | 2.22 MB | 2.31 MB | 458.78 kB | 488.28 kB | 4 | antd.js -10.95 MB | 3.34 MB | 3.49 MB | 855.24 kB | 915.50 kB | 4 | typescript.js +10.95 MB | 3.34 MB | 3.49 MB | 854.83 kB | 915.50 kB | 4 | typescript.js diff --git a/tasks/track_memory_allocations/allocs_minifier.snap b/tasks/track_memory_allocations/allocs_minifier.snap index 1b715b30a105b..60f1b2d57ddfc 100644 --- a/tasks/track_memory_allocations/allocs_minifier.snap +++ b/tasks/track_memory_allocations/allocs_minifier.snap @@ -4,5 +4,5 @@ RadixUIAdoptionSection.jsx | 2.52 kB || 85 | 4 | pdf.mjs | 567.30 kB || 19599 | 2910 || 47403 | 7782 | 1.624 MB -antd.js | 6.69 MB || 99860 | 13518 || 331725 | 70354 | 17.408 MB +antd.js | 6.69 MB || 99745 | 13520 || 331917 | 70164 | 17.367 MB