perf(minifier): unify merging of last expression into target sequence (#26264)

adds new helper `merge_last_expression_into_sequence` and uses it
everywhere when we want to merge last expression to target expression
`a; b;` -> `a, b;`

this change avoids creation of 2 dummy nodes and unnecessary
`drop_statement` on dummy stmt per merge
This commit is contained in:
Armano authored and GitHub committed 2026-09-07 20:40:49 +08:00
1 parent 2ef8e266e1
commit 5303f5c884
2 files changed
+65 -81

No files matched your search

@@ -169,13 +169,27 @@ impl<'a> PeepholeOptimizations {
}
}
fn merge_last_expression_into_sequence(
target: &mut Expression<'a>,
result: &mut ArenaVec<'a, Statement<'a>>,
ctx: &mut TraverseCtx<'a>,
) {
if !ctx.options().sequences
|| !matches!(result.last(), Some(Statement::ExpressionStatement(_)))
{
return;
}
let last_epr = result.pop().unwrap();
let Statement::ExpressionStatement(prev_expr_stmt) = last_epr else { unreachable!() };
let a = prev_expr_stmt.unbox().expression;
ctx.replace_expression_with(target, |b, ctx| Self::join_sequence(a, b, ctx));
}
fn join_sequence(
a: &mut Expression<'a>,
b: &mut Expression<'a>,
a: Expression<'a>,
b: Expression<'a>,
ctx: &TraverseCtx<'a>,
) -> Expression<'a> {
let a = a.take_in(ctx);
let b = b.take_in(ctx);
if let Expression::SequenceExpression(mut sequence_expr) = a {
// `(a, b); c`
sequence_expr.expressions.push(b);
@@ -300,16 +314,10 @@ impl<'a> PeepholeOptimizations {
return;
}
if ctx.options().sequences
&& let Some(Statement::ExpressionStatement(prev_expr_stmt)) = result.last_mut()
{
let a = &mut prev_expr_stmt.expression;
let b = &mut expr_stmt.expression;
expr_stmt.expression = Self::join_sequence(a, b, ctx);
let dropped = result.pop().unwrap();
ctx.drop_statement(&dropped);
}
// "var a; a = b();" => "var a = b();"
// `a; b;` => `a, b;`
Self::merge_last_expression_into_sequence(&mut expr_stmt.expression, result, ctx);
// `var a; a = b();` => `var a = b();`
if Self::merge_leading_assignments_to_declaration(
&mut expr_stmt.expression,
true,
@@ -457,17 +465,10 @@ impl<'a> PeepholeOptimizations {
false,
);
if ctx.options().sequences
&& let Some(Statement::ExpressionStatement(prev_expr_stmt)) = result.last_mut()
{
let a = &mut prev_expr_stmt.expression;
let b = &mut switch_stmt.discriminant;
switch_stmt.discriminant = Self::join_sequence(a, b, ctx);
let dropped = result.pop().unwrap();
ctx.drop_statement(&dropped);
}
// `a; switch(b){}` => `switch(a, b){}`
Self::merge_last_expression_into_sequence(&mut switch_stmt.discriminant, result, ctx);
// "var a; switch (a = b(), c) {}" => "var a = b(); switch (c) {}"
// `var a; switch (a = b(), c) {}` => `var a = b(); switch (c) {}`
Self::merge_leading_assignments_to_declaration(
&mut switch_stmt.discriminant,
false,
@@ -538,19 +539,14 @@ impl<'a> PeepholeOptimizations {
) {
Self::substitute_single_use_symbol_in_statement(&mut if_stmt.test, result, ctx, false);
// "var a; if (a = b(), c) d;" => "var a = b(); if (c) d;"
// `var a; if (a = b(), c) d;` => `var a = b(); if (c) d;`
Self::merge_leading_assignments_to_declaration(&mut if_stmt.test, false, result, ctx);
// `a; if (b) c;` => `if (a, b) c;`
Self::merge_last_expression_into_sequence(&mut if_stmt.test, result, ctx);
// Absorb a previous expression statement
if ctx.options().sequences {
if let Some(Statement::ExpressionStatement(prev_expr_stmt)) = result.last_mut() {
let a = &mut prev_expr_stmt.expression;
let b = &mut if_stmt.test;
if_stmt.test = Self::join_sequence(a, b, ctx);
let dropped = result.pop().unwrap();
ctx.drop_statement(&dropped);
}
if if_stmt.consequent.is_jump_statement() {
// Absorb a previous if statement
if let Some(Statement::IfStatement(prev_if_stmt)) = result.last_mut()
@@ -657,35 +653,33 @@ impl<'a> PeepholeOptimizations {
Self::merge_leading_assignments_to_declaration(ret_argument_expr, false, result, ctx);
}
if let Some(argument) = &mut ret_stmt.argument
if let Some(argument) = &ret_stmt.argument
&& argument.value_type(ctx) == ValueType::Undefined
// `return undefined` has a different semantic in async generator function.
&& !ctx.is_closest_function_scope_an_async_generator()
{
let argument = ret_stmt.argument.take().unwrap();
if argument.may_have_side_effects(ctx) {
if ctx.options().sequences
&& let Some(Statement::ExpressionStatement(prev_expr_stmt)) = result.last_mut()
{
let a = &mut prev_expr_stmt.expression;
prev_expr_stmt.expression = Self::join_sequence(a, argument, ctx);
// `x; return a,void 0;` -> `x,a,void 0; return;`
ctx.replace_expression_with(&mut prev_expr_stmt.expression, |a, ctx| {
Self::join_sequence(a, argument, ctx)
});
} else {
// `return a,void 0;` -> `a,void 0; return;`
let span = argument.span();
let argument = ret_stmt.argument.take().unwrap();
result.push(Statement::new_expression_statement(span, argument, ctx));
ctx.notice_change();
}
} else {
// `return void 0;` -> `return;`
ctx.drop_expression(&argument);
}
if let Some(old) = ret_stmt.argument.take() {
ctx.drop_expression(&old);
}
} else if ctx.options().sequences
&& let Some(argument) = &mut ret_stmt.argument
&& let Some(Statement::ExpressionStatement(prev_expr_stmt)) = result.last_mut()
{
let a = &mut prev_expr_stmt.expression;
let new_arg = Self::join_sequence(a, argument, ctx);
ctx.replace_expression(argument, new_arg);
result.pop();
} else if let Some(argument) = &mut ret_stmt.argument {
// `a; return b;` => `return b, c;`
Self::merge_last_expression_into_sequence(argument, result, ctx);
}
// `if (a) return b; return c;` => `return a ? b : c;`
@@ -781,16 +775,8 @@ impl<'a> PeepholeOptimizations {
false,
);
// `a; throw x` => `throw a, x`
if ctx.options().sequences
&& let Some(Statement::ExpressionStatement(prev_expr_stmt)) = result.last_mut()
{
let a = &mut prev_expr_stmt.expression;
let b = &mut throw_stmt.argument;
throw_stmt.argument = Self::join_sequence(a, b, ctx);
let dropped = result.pop().unwrap();
ctx.drop_statement(&dropped);
}
// `a; throw b;` => `throw a, b;`
Self::merge_last_expression_into_sequence(&mut throw_stmt.argument, result, ctx);
// `var a; throw a = b(), c;` => `var a = b(); throw c;`
Self::merge_leading_assignments_to_declaration(
@@ -902,14 +888,11 @@ impl<'a> PeepholeOptimizations {
if ctx.options().sequences {
match result.last_mut() {
Some(Statement::ExpressionStatement(prev_expr_stmt)) => {
Some(Statement::ExpressionStatement(_)) => {
if let Some(init) = &mut for_stmt.init {
if let Some(init) = init.as_expression_mut() {
let a = &mut prev_expr_stmt.expression;
let new_init = Self::join_sequence(a, init, ctx);
ctx.replace_expression(init, new_init);
let dropped = result.pop().unwrap();
ctx.drop_statement(&dropped);
// `a; for (b;;) c;` => `for (a, b;;) c;`
Self::merge_last_expression_into_sequence(init, result, ctx);
}
} else {
let previous = result.pop().unwrap();
@@ -979,7 +962,7 @@ impl<'a> PeepholeOptimizations {
if ctx.options().sequences {
match result.last_mut() {
// "a; for (var b in c) d" => "for (var b in a, c) d"
Some(Statement::ExpressionStatement(prev_expr_stmt)) => {
Some(Statement::ExpressionStatement(_)) => {
// Annex B.3.5 allows initializers in non-strict mode
// <https://tc39.es/ecma262/multipage/additional-ecmascript-features-for-web-browsers.html#sec-initializers-in-forin-statement-heads>
// Only allow inlining when the for-in variable is declared with `var` and
@@ -1006,10 +989,11 @@ impl<'a> PeepholeOptimizations {
true
};
if can_inline {
let a = &mut prev_expr_stmt.expression;
for_in_stmt.right = Self::join_sequence(a, &mut for_in_stmt.right, ctx);
let dropped = result.pop().unwrap();
ctx.drop_statement(&dropped);
Self::merge_last_expression_into_sequence(
&mut for_in_stmt.right,
result,
ctx,
);
}
}
// "var a; for (a in b) c" => "for (var a in b) c"
@@ -5,9 +5,9 @@ checker.ts:
sys deallocs: 150
sys alloc bytes: 14996632 # 15.00 MB
sys peak growth: 10169560 # 10.17 MB
arena allocs: 77041
arena allocs: 72029
arena reallocs: 28269
arena size: 17492768 # 17.49 MB
arena size: 17401952 # 17.40 MB
App.tsx:
file size: 415342 # 415.34 kB
@@ -16,9 +16,9 @@ App.tsx:
sys deallocs: 61
sys alloc bytes: 2128563 # 2.13 MB
sys peak growth: 1612427 # 1.61 MB
arena allocs: 9590
arena allocs: 8372
arena reallocs: 2709
arena size: 2741920 # 2.74 MB
arena size: 2719488 # 2.72 MB
RadixUIAdoptionSection.jsx:
file size: 2520 # 2.52 kB
@@ -38,9 +38,9 @@ pdf.mjs:
sys deallocs: 2427
sys alloc bytes: 5316219 # 5.32 MB
sys peak growth: 3693363 # 3.69 MB
arena allocs: 26475
arena allocs: 21027
arena reallocs: 7699
arena size: 5889456 # 5.89 MB
arena size: 5783984 # 5.78 MB
antd.js:
file size: 6686316 # 6.69 MB
@@ -49,9 +49,9 @@ antd.js:
sys deallocs: 1038
sys alloc bytes: 29976375 # 29.98 MB
sys peak growth: 19934749 # 19.93 MB
arena allocs: 221812
arena allocs: 207576
arena reallocs: 75941
arena size: 45729992 # 45.73 MB
arena size: 45456840 # 45.46 MB
binder.ts:
file size: 193077 # 193.08 kB
@@ -60,9 +60,9 @@ binder.ts:
sys deallocs: 33
sys alloc bytes: 963032 # 963.03 kB
sys peak growth: 657404 # 657.40 kB
arena allocs: 4099
arena allocs: 3311
arena reallocs: 834
arena size: 1098864 # 1.10 MB
arena size: 1083312 # 1.08 MB
kitchen-sink.tsx:
file size: 732898 # 732.90 kB
@@ -71,6 +71,6 @@ kitchen-sink.tsx:
sys deallocs: 1854
sys alloc bytes: 5304692 # 5.30 MB
sys peak growth: 3707457 # 3.71 MB
arena allocs: 34448
arena allocs: 32092
arena reallocs: 12187
arena size: 8732520 # 8.73 MB
arena size: 8691304 # 8.69 MB