diff --git a/crates/oxc_minifier/src/peephole/inline.rs b/crates/oxc_minifier/src/peephole/inline.rs index a2e1871da6ea4..8568bb2d16ad3 100644 --- a/crates/oxc_minifier/src/peephole/inline.rs +++ b/crates/oxc_minifier/src/peephole/inline.rs @@ -1,5 +1,5 @@ use oxc_ast::ast::*; -use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ConstantValue}; +use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ConstantValue, IsLiteralValue}; use oxc_semantic::{ScopeFlags, ScopeId, SymbolId}; use oxc_span::GetSpan; use oxc_traverse::Ancestor; @@ -111,6 +111,8 @@ impl<'a> PeepholeOptimizations { } else { SymbolValue::Primitive(value) } + } else if init.is_some_and(|init| init.is_literal_value(true, ctx)) { + SymbolValue::ScopedLiteral } else { return; }; @@ -153,6 +155,15 @@ impl<'a> PeepholeOptimizations { ctx.state.changed = true; } } + SymbolValue::ScopedLiteral => { + if symbol_value.read_references_count == 1 + && symbol_value.scope_id.is_some_and(|declared_scope_id| { + Self::is_referenced_in_same_non_control_scope(declared_scope_id, ctx) + }) + { + ctx.state.symbol_values.mark_symbol_inlineable(symbol_id); + } + } SymbolValue::Unknown => {} } } @@ -180,6 +191,23 @@ impl<'a> PeepholeOptimizations { }) .unwrap_or_default() } + + fn is_referenced_in_same_non_control_scope( + declared_scope_id: ScopeId, + ctx: &Ctx<'a, '_>, + ) -> bool { + #[expect(clippy::unnecessary_find_map)] // TODO + ctx.scoping() + .scope_ancestors(ctx.current_scope_id()) + .find_map(|scope_id| { + if declared_scope_id == scope_id { + return Some(true); + } + // TODO: allow non-control scope + Some(false) + }) + .unwrap_or_default() + } } #[cfg(test)] diff --git a/crates/oxc_minifier/src/peephole/mod.rs b/crates/oxc_minifier/src/peephole/mod.rs index 53756de47c3f4..78d812cf311cf 100644 --- a/crates/oxc_minifier/src/peephole/mod.rs +++ b/crates/oxc_minifier/src/peephole/mod.rs @@ -16,9 +16,9 @@ mod remove_unused_expression; mod replace_known_methods; mod substitute_alternate_syntax; -use oxc_ast_visit::Visit; -use oxc_semantic::ReferenceId; -use rustc_hash::FxHashSet; +use oxc_ast_visit::{Visit, VisitMut, walk_mut}; +use oxc_semantic::{ReferenceId, Scoping, SymbolId}; +use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use oxc_allocator::Vec; use oxc_ast::ast::*; @@ -115,6 +115,12 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations { fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { self.changed = ctx.state.changed; + let inlineable_symbols = ctx.state.symbol_values.get_inlineable_symbols(); + if !inlineable_symbols.is_empty() { + let mut value_inliner = ValueInliner::new(inlineable_symbols, ctx.scoping()); + value_inliner.visit_program(program); + self.changed = true; + } if self.changed { // Remove unused references by visiting the AST again and diff the collected references. let refs_before = @@ -525,3 +531,47 @@ impl<'a> Visit<'a> for ReferencesCounter { self.refs.insert(reference_id); } } + +struct ValueInliner<'a, 'b> { + scoping: &'b Scoping, + inlineable_symbols: &'b FxHashSet, + values: FxHashMap>, +} + +impl<'b> ValueInliner<'_, 'b> { + pub fn new(inlineable_symbols: &'b FxHashSet, scoping: &'b Scoping) -> Self { + Self { + scoping, + inlineable_symbols, + values: FxHashMap::with_capacity_and_hasher(inlineable_symbols.len(), FxBuildHasher), + } + } +} + +impl<'a> VisitMut<'a> for ValueInliner<'a, '_> { + fn visit_variable_declarators(&mut self, decls: &mut Vec<'a, VariableDeclarator<'a>>) { + walk_mut::walk_variable_declarators(self, decls); + for decl in decls { + if let BindingPatternKind::BindingIdentifier(id) = &mut decl.id.kind { + let symbol_id = id.symbol_id(); + if self.inlineable_symbols.contains(&symbol_id) { + self.values.insert( + symbol_id, + decl.init.take().expect("Expected initializer for inlineable symbols"), + ); + } + } + } + } + + fn visit_expression(&mut self, it: &mut Expression<'a>) { + walk_mut::walk_expression(self, it); + if let Expression::Identifier(id) = it { + if let Some(symbol_id) = self.scoping.get_reference(id.reference_id()).symbol_id() + && let Some(value) = self.values.remove(&symbol_id) + { + *it = value; + } + } + } +} diff --git a/crates/oxc_minifier/src/symbol_value.rs b/crates/oxc_minifier/src/symbol_value.rs index b3fe50a23041c..fcc198d5feccb 100644 --- a/crates/oxc_minifier/src/symbol_value.rs +++ b/crates/oxc_minifier/src/symbol_value.rs @@ -1,4 +1,4 @@ -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use oxc_ecmascript::constant_evaluation::ConstantValue; use oxc_syntax::{scope::ScopeId, symbol::SymbolId}; @@ -10,6 +10,9 @@ pub enum SymbolValue<'a> { /// Initialized primitive value. /// This can be inlined within the same scope after the variable is declared. ScopedPrimitive(ConstantValue<'a>), + /// Initialized scoped literal value. + /// This can be inlined within the same scope after the variable is declared if it's only used once. + ScopedLiteral, #[default] Unknown, } @@ -36,18 +39,25 @@ pub struct SymbolInformation<'a> { #[derive(Debug, Default)] pub struct SymbolInformationMap<'a> { values: FxHashMap>, + inlineable_symbols: FxHashSet, } impl<'a> SymbolInformationMap<'a> { pub fn clear(&mut self) { self.values.clear(); + self.inlineable_symbols.clear(); } pub fn init_value(&mut self, symbol_id: SymbolId, symbol_value: SymbolInformation<'a>) { self.values.insert(symbol_id, symbol_value); } - pub fn set_value(&mut self, symbol_id: SymbolId, symbol_value: SymbolValue<'a>, scope_id: ScopeId) { + pub fn set_value( + &mut self, + symbol_id: SymbolId, + symbol_value: SymbolValue<'a>, + scope_id: ScopeId, + ) { let info = self.values.get_mut(&symbol_id).expect("symbol value must exist"); info.value = symbol_value; info.scope_id = Some(scope_id); @@ -56,4 +66,12 @@ impl<'a> SymbolInformationMap<'a> { pub fn get_symbol_value(&self, symbol_id: SymbolId) -> Option<&SymbolInformation<'a>> { self.values.get(&symbol_id) } + + pub fn mark_symbol_inlineable(&mut self, symbol_id: SymbolId) { + self.inlineable_symbols.insert(symbol_id); + } + + pub fn get_inlineable_symbols(&self) -> &FxHashSet { + &self.inlineable_symbols + } } diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 9f73527dc712e..0af4321a6e6b3 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.14 kB | 23.70 kB | 8.32 kB | 8.54 kB | 2 | react.development.js +72.14 kB | 23.12 kB | 23.70 kB | 8.32 kB | 8.54 kB | 2 | react.development.js -173.90 kB | 59.33 kB | 59.82 kB | 19.08 kB | 19.33 kB | 2 | moment.js +173.90 kB | 59.27 kB | 59.82 kB | 19.04 kB | 19.33 kB | 2 | moment.js -287.63 kB | 89.28 kB | 90.07 kB | 30.93 kB | 31.95 kB | 2 | jquery.js +287.63 kB | 89.27 kB | 90.07 kB | 30.93 kB | 31.95 kB | 2 | jquery.js -342.15 kB | 116.94 kB | 118.14 kB | 43.14 kB | 44.37 kB | 2 | vue.js +342.15 kB | 116.92 kB | 118.14 kB | 43.13 kB | 44.37 kB | 2 | vue.js -544.10 kB | 71.89 kB | 72.48 kB | 25.48 kB | 26.20 kB | 2 | lodash.js +544.10 kB | 71.89 kB | 72.48 kB | 25.49 kB | 26.20 kB | 2 | lodash.js -555.77 kB | 270.62 kB | 270.13 kB | 88.13 kB | 90.80 kB | 2 | d3.js +555.77 kB | 270.61 kB | 270.13 kB | 88.14 kB | 90.80 kB | 2 | d3.js -1.01 MB | 439.47 kB | 458.89 kB | 122.10 kB | 126.71 kB | 4 | bundle.min.js +1.01 MB | 439.45 kB | 458.89 kB | 122.08 kB | 126.71 kB | 4 | bundle.min.js -1.25 MB | 644.77 kB | 646.76 kB | 158.99 kB | 163.73 kB | 2 | three.js +1.25 MB | 644.71 kB | 646.76 kB | 158.90 kB | 163.73 kB | 3 | three.js -2.14 MB | 713.33 kB | 724.14 kB | 160.72 kB | 181.07 kB | 2 | victory.js +2.14 MB | 711.05 kB | 724.14 kB | 159.75 kB | 181.07 kB | 5 | victory.js -3.20 MB | 1.00 MB | 1.01 MB | 322.70 kB | 331.56 kB | 3 | echarts.js +3.20 MB | 1.00 MB | 1.01 MB | 322.78 kB | 331.56 kB | 3 | echarts.js -6.69 MB | 2.22 MB | 2.31 MB | 458.78 kB | 488.28 kB | 4 | antd.js +6.69 MB | 2.22 MB | 2.31 MB | 457.74 kB | 488.28 kB | 4 | antd.js -10.95 MB | 3.34 MB | 3.49 MB | 854.83 kB | 915.50 kB | 4 | typescript.js +10.95 MB | 3.34 MB | 3.49 MB | 854.56 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 60f1b2d57ddfc..d9c1c51b72688 100644 --- a/tasks/track_memory_allocations/allocs_minifier.snap +++ b/tasks/track_memory_allocations/allocs_minifier.snap @@ -2,7 +2,7 @@ File | File size || Sys allocs | Sys reallocs | ------------------------------------------------------------------------------------------------------------------------------------------- RadixUIAdoptionSection.jsx | 2.52 kB || 85 | 4 || 20 | 6 | 688 B -pdf.mjs | 567.30 kB || 19599 | 2910 || 47403 | 7782 | 1.624 MB +pdf.mjs | 567.30 kB || 19786 | 2914 || 56295 | 10084 | 1.998 MB -antd.js | 6.69 MB || 99745 | 13520 || 331917 | 70164 | 17.367 MB +antd.js | 6.69 MB || 98808 | 13511 || 331792 | 70073 | 17.306 MB