diff --git a/crates/oxc_minifier/src/ast_passes/mod.rs b/crates/oxc_minifier/src/ast_passes/mod.rs index b0269f4ddd840d..6f8f2a06bfc50a 100644 --- a/crates/oxc_minifier/src/ast_passes/mod.rs +++ b/crates/oxc_minifier/src/ast_passes/mod.rs @@ -1,5 +1,6 @@ use oxc_allocator::Vec; use oxc_ast::ast::*; +use oxc_syntax::es_target::ESTarget; use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx}; mod collapse_variable_declarations; @@ -53,14 +54,14 @@ pub struct PeepholeOptimizations { impl PeepholeOptimizations { /// `in_fixed_loop`: Do not compress syntaxes that are hard to analyze inside the fixed loop. /// Opposite of `late` in Closure Compiler. - pub fn new(in_fixed_loop: bool, options: CompressOptions) -> Self { + pub fn new(target: ESTarget, in_fixed_loop: bool, options: CompressOptions) -> Self { Self { x0_statement_fusion: StatementFusion::new(), x1_minimize_exit_points: MinimizeExitPoints::new(), x2_exploit_assigns: ExploitAssigns::new(), x3_collapse_variable_declarations: CollapseVariableDeclarations::new(), x4_peephole_remove_dead_code: PeepholeRemoveDeadCode::new(), - x5_peephole_minimize_conditions: PeepholeMinimizeConditions::new(), + x5_peephole_minimize_conditions: PeepholeMinimizeConditions::new(target), x6_peephole_substitute_alternate_syntax: PeepholeSubstituteAlternateSyntax::new( options.target, in_fixed_loop, diff --git a/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs b/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs index 3549d6920ba181..302b114b736565 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs @@ -2,6 +2,7 @@ use oxc_allocator::Vec; use oxc_ast::{ast::*, NONE}; use oxc_ecmascript::constant_evaluation::ValueType; use oxc_span::{cmp::ContentEq, GetSpan, SPAN}; +use oxc_syntax::es_target::ESTarget; use oxc_traverse::{traverse_mut_with_ctx, Ancestor, ReusableTraverseCtx, Traverse, TraverseCtx}; use crate::CompressorPass; @@ -14,6 +15,7 @@ use crate::CompressorPass; /// /// pub struct PeepholeMinimizeConditions { + target: ESTarget, pub(crate) changed: bool, } @@ -56,7 +58,7 @@ impl<'a> Traverse<'a> for PeepholeMinimizeConditions { Expression::UnaryExpression(e) => Self::try_minimize_not(e, ctx), Expression::LogicalExpression(e) => Self::try_minimize_logical(e, ctx), Expression::BinaryExpression(e) => Self::try_minimize_binary(e, ctx), - Expression::ConditionalExpression(e) => Self::try_minimize_conditional(e, ctx), + Expression::ConditionalExpression(e) => self.try_minimize_conditional(e, ctx), _ => None, } { *expr = folded_expr; @@ -66,8 +68,8 @@ impl<'a> Traverse<'a> for PeepholeMinimizeConditions { } impl<'a> PeepholeMinimizeConditions { - pub fn new() -> Self { - Self { changed: false } + pub fn new(target: ESTarget) -> Self { + Self { target, changed: false } } /// Try to minimize NOT nodes such as `!(x==y)`. @@ -344,6 +346,7 @@ impl<'a> PeepholeMinimizeConditions { // based on https://github.com/evanw/esbuild/blob/df815ac27b84f8b34374c9182a93c94718f8a630/internal/js_ast/js_ast_helpers.go#L2745 fn try_minimize_conditional( + &self, expr: &mut ConditionalExpression<'a>, ctx: &mut TraverseCtx<'a>, ) -> Option> { @@ -633,7 +636,39 @@ impl<'a> PeepholeMinimizeConditions { } } - // TODO: Try using the "??" or "?." operators + // Try using the "??" or "?." operators + if let Expression::BinaryExpression(bin_expr) = &mut expr.test { + if bin_expr.operator == BinaryOperator::Equality + || bin_expr.operator == BinaryOperator::Inequality + { + if let Some(check) = { + if bin_expr.left.is_null() { + Some(&bin_expr.right) + } else { + Some(&bin_expr.left) + } + } { + // `a != null ? a : b` -> `a ?? b`` + if check.content_eq(if bin_expr.operator == BinaryOperator::Equality { + &expr.alternate + } else { + &expr.consequent + }) && self.target >= ESTarget::ES2020 + // TODO: this is probably a but too aggressive + && matches!(check, Expression::Identifier(_)) + { + return Some(ctx.ast.expression_logical( + SPAN, + ctx.ast.move_expression(&mut expr.consequent), + LogicalOperator::Coalesce, + ctx.ast.move_expression(&mut expr.alternate), + )); + } + + // TODO: `a != null ? a.b.c[d](e) : undefined` -> `a?.b.c[d](e)`` + } + } + } // Non esbuild optimizations @@ -812,12 +847,13 @@ impl<'a> PeepholeMinimizeConditions { #[cfg(test)] mod test { use oxc_allocator::Allocator; + use oxc_syntax::es_target::ESTarget; use crate::tester; fn test(source_text: &str, positive: &str) { let allocator = Allocator::default(); - let mut pass = super::PeepholeMinimizeConditions::new(); + let mut pass = super::PeepholeMinimizeConditions::new(ESTarget::ES2025); tester::test(&allocator, source_text, positive, &mut pass); } @@ -2003,7 +2039,8 @@ mod test { test("var a; a ? b(c, d) : b(e, d)", "var a; b(a ? c : e, d)"); test("var a; a ? b(...c) : b(...e)", "var a; b(...a ? c : e)"); test("var a; a ? b(c) : b(e)", "var a; b(a ? c : e)"); - // test("a != null ? a : b", "a ?? b"); + test("a != null ? a : b", "a ?? b"); + test_same("a() != null ? a() : b"); // test("a != null ? a.b.c[d](e) : undefined", "a?.b.c[d](e)"); } } diff --git a/crates/oxc_minifier/src/compressor.rs b/crates/oxc_minifier/src/compressor.rs index be8d6bf1fa7d9f..816695e0b62a86 100644 --- a/crates/oxc_minifier/src/compressor.rs +++ b/crates/oxc_minifier/src/compressor.rs @@ -34,8 +34,10 @@ impl<'a> Compressor<'a> { RemoveSyntax::new(self.options).build(program, &mut ctx); // RemoveUnusedCode::new(self.options).build(program, &mut ctx); Normalize::new().build(program, &mut ctx); - PeepholeOptimizations::new(true, self.options).run_in_loop(program, &mut ctx); - PeepholeOptimizations::new(false, self.options).build(program, &mut ctx); + PeepholeOptimizations::new(self.options.target, true, self.options) + .run_in_loop(program, &mut ctx); + PeepholeOptimizations::new(self.options.target, false, self.options) + .build(program, &mut ctx); } pub fn dead_code_elimination(self, program: &mut Program<'a>) { diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 827137a019df6b..abd7b9c2026af1 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -3,25 +3,25 @@ Original | minified | minified | gzip | gzip | Fixture ------------------------------------------------------------------------------------- 72.14 kB | 23.70 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js -173.90 kB | 59.80 kB | 59.82 kB | 19.41 kB | 19.33 kB | moment.js +173.90 kB | 59.78 kB | 59.82 kB | 19.40 kB | 19.33 kB | moment.js -287.63 kB | 90.13 kB | 90.07 kB | 32.05 kB | 31.95 kB | jquery.js +287.63 kB | 90.07 kB | 90.07 kB | 32.03 kB | 31.95 kB | jquery.js -342.15 kB | 118.36 kB | 118.14 kB | 44.52 kB | 44.37 kB | vue.js +342.15 kB | 118.35 kB | 118.14 kB | 44.51 kB | 44.37 kB | vue.js -544.10 kB | 71.74 kB | 72.48 kB | 26.14 kB | 26.20 kB | lodash.js +544.10 kB | 71.72 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js -555.77 kB | 273.19 kB | 270.13 kB | 90.92 kB | 90.80 kB | d3.js +555.77 kB | 272.90 kB | 270.13 kB | 90.85 kB | 90.80 kB | d3.js -1.01 MB | 460.47 kB | 458.89 kB | 126.83 kB | 126.71 kB | bundle.min.js +1.01 MB | 460.43 kB | 458.89 kB | 126.82 kB | 126.71 kB | bundle.min.js 1.25 MB | 652.88 kB | 646.76 kB | 163.52 kB | 163.73 kB | three.js -2.14 MB | 726.28 kB | 724.14 kB | 180.14 kB | 181.07 kB | victory.js +2.14 MB | 726.04 kB | 724.14 kB | 180.07 kB | 181.07 kB | victory.js -3.20 MB | 1.01 MB | 1.01 MB | 331.93 kB | 331.56 kB | echarts.js +3.20 MB | 1.01 MB | 1.01 MB | 331.81 kB | 331.56 kB | echarts.js -6.69 MB | 2.32 MB | 2.31 MB | 492.68 kB | 488.28 kB | antd.js +6.69 MB | 2.32 MB | 2.31 MB | 492.63 kB | 488.28 kB | antd.js -10.95 MB | 3.50 MB | 3.49 MB | 908.82 kB | 915.50 kB | typescript.js +10.95 MB | 3.50 MB | 3.49 MB | 908.61 kB | 915.50 kB | typescript.js