perf(minifier): reduce allocations when processing if statements (#26666)

- introduce context function `replace_statement_with` for replacing
stmts in place
- `try_minimize_if` rewrite statements to new statements in place
- update `if` stmt instead of recreating it in `handle_if_statement`
This commit is contained in:
Armano authored and GitHub committed 2026-09-16 14:35:54 +08:00
1 parent 2ce913e33e
commit 5f729541b4
5 files changed
+107 -71

No files matched your search

@@ -10,32 +10,40 @@ use super::PeepholeOptimizations;
impl<'a> PeepholeOptimizations {
/// `MangleIf`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_parser/js_parser.go#L9860>
pub fn try_minimize_if(
if_stmt: &mut IfStatement<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Statement<'a>> {
pub fn try_minimize_if(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
let Statement::IfStatement(if_stmt) = stmt else { return };
// Flip empty consequent so the rest of the function can assume consequent is non-empty.
if Self::is_statement_empty(&if_stmt.consequent) {
if if_stmt.alternate.is_none() {
// `if (a) {}` => `a;`
let mut expr = if_stmt.test.take_in(ctx);
Self::remove_unused_expression(&mut expr, ctx);
return Some(Statement::new_expression_statement(if_stmt.span, expr, ctx));
ctx.replace_statement_with(stmt, |stmt, ctx| {
let Statement::IfStatement(if_stmt) = stmt else { unreachable!() };
let IfStatement { mut test, span, .. } = if_stmt.unbox();
Self::remove_unused_expression(&mut test, ctx);
Statement::new_expression_statement(span, test, ctx)
});
return;
}
let mut new_consequent = if_stmt.alternate.take().unwrap();
let new_consequent = if_stmt.alternate.take().unwrap();
if let Statement::ExpressionStatement(expr_stmt) = &mut new_consequent {
let (op, a) = match &mut if_stmt.test {
// `if (!a); else b();` => `a && b();`
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
(LogicalOperator::And, unary_expr.argument.take_in(ctx))
}
// `if (a); else b();` => `a || b();`
e => (LogicalOperator::Or, e.take_in(ctx)),
};
let b = expr_stmt.expression.take_in(ctx);
let expr = Self::join_with_left_associative_op(if_stmt.span, op, a, b, ctx);
return Some(Statement::new_expression_statement(if_stmt.span, expr, ctx));
if let Statement::ExpressionStatement(expr_stmt) = new_consequent {
ctx.replace_statement_with(stmt, |stmt, ctx| {
let Statement::IfStatement(if_stmt) = stmt else { unreachable!() };
let IfStatement { test, span, .. } = if_stmt.unbox();
let (op, a) = match test {
// `if (!a); else b();` => `a && b();`
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
(LogicalOperator::And, unary_expr.unbox().argument)
}
// `if (a); else b();` => `a || b();`
e => (LogicalOperator::Or, e),
};
let b = expr_stmt.unbox().expression;
let expr = Self::join_with_left_associative_op(span, op, a, b, ctx);
Statement::new_expression_statement(span, expr, ctx)
});
return;
}
// `if (!a) {} else x;` => `if (a) x;`
@@ -48,41 +56,56 @@ impl<'a> PeepholeOptimizations {
// Consequent is non-empty from here on.
if let Some(alternate) = &mut if_stmt.alternate {
if let Statement::ExpressionStatement(expr_stmt) = &mut if_stmt.consequent {
if let Statement::ExpressionStatement(alternate_expr_stmt) = alternate {
if let Some(alternate) = &if_stmt.alternate {
if matches!(&if_stmt.consequent, Statement::ExpressionStatement(_)) {
if matches!(alternate, Statement::ExpressionStatement(_)) {
// `if (a) b(); else c();` => `a ? b() : c();`
let test = if_stmt.test.take_in(ctx);
let consequent = expr_stmt.expression.take_in(ctx);
let alternate = alternate_expr_stmt.expression.take_in(ctx);
let expr =
Self::minimize_conditional(if_stmt.span, test, consequent, alternate, ctx);
return Some(Statement::new_expression_statement(if_stmt.span, expr, ctx));
ctx.replace_statement_with(stmt, |stmt, ctx| {
let Statement::IfStatement(if_stmt) = stmt else { unreachable!() };
let IfStatement { test, consequent, alternate, span, .. } = if_stmt.unbox();
let Statement::ExpressionStatement(a) = consequent else { unreachable!() };
let Statement::ExpressionStatement(b) = alternate.unwrap() else {
unreachable!()
};
let a = a.unbox().expression;
let b = b.unbox().expression;
let expr = Self::minimize_conditional(span, test, a, b, ctx);
Statement::new_expression_statement(span, expr, ctx)
});
return;
}
} else {
// Normalize: move the `!` out of the test by swapping branches.
// Avoid swapping when alternate is an `if` — that risks a worse chain.
// `if (!a) return b; else return c;` => `if (a) return c; else return b;`
if !matches!(alternate, Statement::IfStatement(_))
&& let Expression::UnaryExpression(unary_expr) = &mut if_stmt.test
&& let Expression::UnaryExpression(unary_expr) = &if_stmt.test
&& unary_expr.operator.is_not()
{
ctx.replace_expression_with(&mut if_stmt.test, Self::unwrap_unary);
std::mem::swap(&mut if_stmt.consequent, alternate);
let if_mut = if_stmt.as_mut();
let Some(alternate) = &mut if_mut.alternate else { unreachable!() };
std::mem::swap(&mut if_mut.consequent, alternate);
}
}
} else if let Statement::ExpressionStatement(expr_stmt) = &mut if_stmt.consequent {
let (op, a) = match &mut if_stmt.test {
// `if (!a) b();` => `a || b();`
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
(LogicalOperator::Or, unary_expr.argument.take_in(ctx))
}
// `if (a) b();` => `a && b();`
e => (LogicalOperator::And, e.take_in(ctx)),
};
let b = expr_stmt.expression.take_in(ctx);
let expr = Self::join_with_left_associative_op(if_stmt.span, op, a, b, ctx);
return Some(Statement::new_expression_statement(if_stmt.span, expr, ctx));
} else if matches!(&if_stmt.consequent, Statement::ExpressionStatement(_)) {
ctx.replace_statement_with(stmt, |stmt, ctx| {
let Statement::IfStatement(if_stmt) = stmt else { unreachable!() };
let IfStatement { test, consequent, span, .. } = if_stmt.unbox();
let Statement::ExpressionStatement(b) = consequent else { unreachable!() };
let (op, a) = match test {
// `if (!a) b();` => `a || b();`
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
(LogicalOperator::Or, unary_expr.unbox().argument)
}
// `if (a) b();` => `a && b();`
e => (LogicalOperator::And, e),
};
let b = b.unbox().expression;
let expr = Self::join_with_left_associative_op(span, op, a, b, ctx);
Statement::new_expression_statement(span, expr, ctx)
});
return;
} else if let Statement::IfStatement(if2_stmt) = &mut if_stmt.consequent
&& if2_stmt.alternate.is_none()
{
@@ -95,7 +118,6 @@ impl<'a> PeepholeOptimizations {
}
Self::wrap_to_avoid_ambiguous_else(if_stmt, ctx);
None
}
/// Wrap to avoid ambiguous else.
@@ -621,20 +621,18 @@ impl<'a> PeepholeOptimizations {
} else {
body[0].span()
};
let test = if_stmt.unbox().test;
let test = Self::minimize_not(test.span(), test, ctx, true);
ctx.replace_expression_with(&mut if_stmt.test, |test, ctx| {
Self::minimize_not(test.span(), test, ctx, true)
});
let consequent = if body.len() == 1 {
body.remove(0)
} else {
let scope_id = ctx.create_child_scope_of_current(ScopeFlags::empty());
Statement::new_block_statement_with_scope_id(span, body, scope_id, ctx)
};
let mut if_stmt =
IfStatement::new(test.span(), test, consequent, None, ctx);
let if_stmt =
Self::try_minimize_if(&mut if_stmt, ctx).unwrap_or_else(|| {
Statement::IfStatement(ArenaBox::new_in(if_stmt, ctx))
});
ctx.replace_statement(&mut if_stmt.consequent, consequent);
let mut if_stmt = Statement::IfStatement(if_stmt);
Self::try_minimize_if(&mut if_stmt, ctx);
ctx.notice_change();
Self::minimize_statement(if_stmt, stmts, result, ctx);
return;
+1 -5
View File
@@ -436,11 +436,7 @@ impl<'a> Traverse<'a> for PeepholeOptimizations {
Statement::IfStatement(s) => {
Self::minimize_expression_in_boolean_context(&mut s.test, ctx);
Self::try_fold_if(stmt, ctx);
if let Statement::IfStatement(if_stmt) = stmt
&& let Some(folded_stmt) = Self::try_minimize_if(if_stmt, ctx)
{
ctx.replace_statement(stmt, folded_stmt);
}
Self::try_minimize_if(stmt, ctx);
}
Statement::WhileStatement(s) => {
Self::minimize_expression_in_boolean_context(&mut s.test, ctx);
@@ -433,7 +433,7 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> {
(options.class && is_class) || (options.function && !is_class)
}
/// Construct a `DroppedSubtreeCollector` borrowing the per-pass change accumulator.
/// Construct a [`DroppedSubtreeCollector`] borrowing the per-pass change accumulator.
/// Used by the `replace_*` / `drop_*` helpers.
#[inline]
fn dropped_subtree_collector(&mut self) -> DroppedSubtreeCollector<'a, '_> {
@@ -442,7 +442,7 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> {
/// Replace an expression slot. Marks the pass as having mutated the AST.
///
/// Prefer this over a direct `*slot = new; ctx.notice_change();` pair —
/// Prefer this over a direct `*slot = new;` and [`Self::notice_change`] pair —
/// the typed helper keeps dropped-subtree bookkeeping, the slot update,
/// and the pass revisit request together.
#[inline]
@@ -469,6 +469,10 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> {
}
/// Replace a statement slot. Marks the pass as having mutated the AST.
///
/// Prefer this over a direct `*slot = new;` and [`Self::notice_change`] pair —
/// the typed helper keeps dropped-subtree bookkeeping, the slot update,
/// and the pass revisit request together.
#[inline]
pub fn replace_statement(&mut self, slot: &mut Statement<'a>, new: Statement<'a>) {
self.dropped_subtree_collector().visit_statement(slot);
@@ -476,6 +480,22 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> {
self.state.record_ast_change();
}
/// Replace a statement slot with a value built from the owned old statement.
///
/// Prefer this over `take_in` followed by [`Self::replace_statement`]. It avoids
/// leaving a dummy node in the arena. The closure must report any discarded
/// subtrees through the `drop_*` helpers; values moved into its result are not dropped.
#[inline]
pub fn replace_statement_with(
&mut self,
slot: &mut Statement<'a>,
replacer: impl FnOnce(Statement<'a>, &mut Self) -> Statement<'a>,
) {
let ctx = &mut *self;
slot.replace_with(|old| replacer(old, ctx));
self.state.record_ast_change();
}
/// Replace an assignment-target-property slot. Marks the pass as having mutated the AST.
#[inline]
pub fn replace_assignment_target_property(
@@ -5,9 +5,9 @@ checker.ts:
sys deallocs: 122
sys alloc bytes: 14996632 # 15.00 MB
sys peak growth: 10169560 # 10.17 MB
arena allocs: 59899
arena allocs: 56226
arena reallocs: 21411
arena size: 16569784 # 16.57 MB
arena size: 16507656 # 16.51 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: 8350
arena allocs: 7489
arena reallocs: 2709
arena size: 2719488 # 2.72 MB
arena size: 2704912 # 2.70 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: 20924
arena allocs: 19526
arena reallocs: 7696
arena size: 5782208 # 5.78 MB
arena size: 5757664 # 5.76 MB
antd.js:
file size: 6686316 # 6.69 MB
@@ -49,9 +49,9 @@ antd.js:
sys deallocs: 860
sys alloc bytes: 29976375 # 29.98 MB
sys peak growth: 19934749 # 19.93 MB
arena allocs: 187025
arena allocs: 181720
arena reallocs: 63323
arena size: 43370040 # 43.37 MB
arena size: 43279864 # 43.28 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: 3311
arena allocs: 2923
arena reallocs: 834
arena size: 1083312 # 1.08 MB
arena size: 1077056 # 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: 31927
arena allocs: 31143
arena reallocs: 12185
arena size: 8688104 # 8.69 MB
arena size: 8673608 # 8.67 MB