perf(minifier): move owned AST nodes directly (#25837)

Several peephole transforms call `take_in` after removing or consuming the parent AST node. At that point there is no borrowed slot to preserve, so allocating a dummy replacement is unnecessary.

Move those already-owned child nodes directly in the straightforward expression and declaration cases. This second branch stays intentionally mechanical and leaves the statement/reference-bookkeeping cases to the final PR in the stack.

Relative to its parent, the tracked allocation fixtures record 22,957 fewer arena allocations and 370,544 fewer arena bytes. Minsize output is unchanged.

Validated with `cargo test -p oxc_minifier`, `just minsize`, `cargo clippy -p oxc_minifier -- -D warnings`, `just fmt`, and `git diff --check`.

AI assistance was used for research, implementation, review, and testing. I reviewed and take responsibility for the changes.
This commit is contained in:
Dunqing committed 2026-08-18 06:57:08 +00:00
1 parent 673b04bc46
commit 61b2aefa94
8 files changed
+72 -93

No files matched your search

@@ -1,4 +1,4 @@
use oxc_allocator::TakeIn;
use oxc_allocator::ReplaceWith;
use oxc_ast::ast::*;
use oxc_syntax::identifier::is_identifier_name_patched;
@@ -22,15 +22,13 @@ impl<'a> PeepholeOptimizations {
let Expression::StringLiteral(s) = &e.expression else { return };
if is_identifier_name_patched(&s.value) {
let property = IdentifierName::new(s.span, s.value, ctx);
let new_member = StaticMemberExpression::boxed(
e.span,
e.object.take_in(ctx),
property,
e.optional,
ctx,
);
// Direct slot write: no typed helper for the `MemberExpression` enum slot; the sibling `notice_change()` preserves the mutation signal.
*expr = MemberExpression::StaticMemberExpression(new_member);
expr.replace_with(|expr| {
let MemberExpression::ComputedMemberExpression(e) = expr else { unreachable!() };
let ComputedMemberExpression { span, object, optional, .. } = e.unbox();
MemberExpression::StaticMemberExpression(StaticMemberExpression::boxed(
span, object, property, optional, ctx,
))
});
ctx.notice_change();
return;
}
@@ -42,17 +42,16 @@ impl<'a> PeepholeOptimizations {
let mut b = b;
// "a op (b op c)" => "(a op b) op c"
// "a op (b op (c op d))" => "((a op b) op c) op d"
loop {
if let Expression::LogicalExpression(logical_expr) = &mut b
&& logical_expr.operator == op
{
let right = logical_expr.left.take_in(ctx);
a = Self::join_with_left_associative_op(span, op, a, right, ctx);
b = logical_expr.right.take_in(ctx);
continue;
let b = loop {
match b {
Expression::LogicalExpression(logical_expr) if logical_expr.operator == op => {
let LogicalExpression { left, right, .. } = logical_expr.unbox();
a = Self::join_with_left_associative_op(span, op, a, left, ctx);
b = right;
}
b => break b,
}
break;
}
};
// "a op b" => "a op b"
// "(a op b) op c" => "(a op b) op c"
let mut logic_expr = Expression::new_logical_expression(span, a, op, b, ctx);
@@ -40,9 +40,10 @@ impl<'a> PeepholeOptimizations {
stmt => (stmt, None),
};
let Statement::IfStatement(mut if_stmt) = first else { unreachable!() };
let Statement::IfStatement(if_stmt) = first else { unreachable!() };
let IfStatement { test, alternate, .. } = if_stmt.unbox();
let expr = match if_stmt.test.take_in(ctx) {
let expr = match test {
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
unary_expr.unbox().argument
}
@@ -61,7 +62,6 @@ impl<'a> PeepholeOptimizations {
for_stmt.test = Some(expr);
}
let alternate = if_stmt.alternate.take();
let new_body = Self::drop_first_statement(span, body, alternate, ctx);
ctx.replace_statement(&mut for_stmt.body, new_body);
return;
@@ -84,9 +84,10 @@ impl<'a> PeepholeOptimizations {
stmt => (stmt, None),
};
let Statement::IfStatement(mut if_stmt) = first else { unreachable!() };
let Statement::IfStatement(if_stmt) = first else { unreachable!() };
let IfStatement { test, consequent, .. } = if_stmt.unbox();
let expr = if_stmt.test.take_in(ctx);
let expr = test;
if let Some(test) = &mut for_stmt.test {
let left = test.take_in(ctx);
@@ -100,7 +101,6 @@ impl<'a> PeepholeOptimizations {
for_stmt.test = Some(expr);
}
let consequent = if_stmt.consequent.take_in(ctx);
let new_body = Self::drop_first_statement(span, body, Some(consequent), ctx);
ctx.replace_statement(&mut for_stmt.body, new_body);
}
@@ -119,7 +119,7 @@ impl<'a> PeepholeOptimizations {
} else if block_stmt.body.len() == 2
&& !Self::statement_cares_about_scope(&block_stmt.body[1])
{
return block_stmt.body[1].take_in(ctx);
return block_stmt.body.pop().unwrap();
} else {
block_stmt.body.remove(0);
}
@@ -248,20 +248,20 @@ impl<'a> PeepholeOptimizations {
}
let Expression::SequenceExpression(sequence_expr) = &mut e.right else { return };
let Some(Expression::AssignmentExpression(mut assignment_expr)) =
let Some(Expression::AssignmentExpression(assignment_expr)) =
sequence_expr.expressions.pop()
else {
unreachable!()
};
Self::mark_assignment_target_as_read(&assignment_expr.left, ctx);
let AssignmentExpression { left, right, .. } = assignment_expr.unbox();
Self::mark_assignment_target_as_read(&left, ctx);
let assign_value = assignment_expr.right.take_in(ctx);
sequence_expr.expressions.push(assign_value);
sequence_expr.expressions.push(right);
let new_expr = Expression::new_assignment_expression(
e.span,
e.operator.to_assignment_operator(),
assignment_expr.left.take_in(ctx),
left,
e.right.take_in(ctx),
ctx,
);
@@ -1,5 +1,5 @@
use crate::generated::ancestor::Ancestor;
use oxc_allocator::{ArenaVec, ReplaceWith, TakeIn};
use oxc_allocator::{ArenaVec, ReplaceWith};
use oxc_ast::ast::*;
use oxc_ecmascript::{
constant_evaluation::{ConstantValue, DetermineValueType, ValueType},
@@ -132,8 +132,11 @@ impl<'a> Traverse<'a> for Normalize {
}
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
if let Expression::ParenthesizedExpression(paren_expr) = expr {
*expr = paren_expr.expression.take_in(ctx);
if matches!(expr, Expression::ParenthesizedExpression(_)) {
expr.replace_with(|expr| {
let Expression::ParenthesizedExpression(paren_expr) = expr else { unreachable!() };
paren_expr.unbox().expression
});
}
// Handled outside the match below so the replacement can go through
// `ctx.replace_expression`, which walks the dropped call (its
@@ -72,28 +72,21 @@ impl<'a> PeepholeOptimizations {
return None;
}
let mut second_arg = arguments.pop().expect("checked len above");
let second_arg = second_arg.to_expression_mut(); // checked above
let mut first_arg = arguments.pop().expect("checked len above");
let first_arg = first_arg.to_expression_mut(); // checked above
let second_arg = arguments.pop().expect("checked len above").into_expression();
let first_arg = arguments.pop().expect("checked len above").into_expression();
let wrap_with_unary_plus_if_needed = |expr: &mut Expression<'a>| {
let wrap_with_unary_plus_if_needed = |expr: Expression<'a>| {
if expr.value_type(ctx).is_number() {
expr.take_in(ctx)
expr
} else {
Expression::new_unary_expression(
SPAN,
UnaryOperator::UnaryPlus,
expr.take_in(ctx),
ctx,
)
Expression::new_unary_expression(SPAN, UnaryOperator::UnaryPlus, expr, ctx)
}
};
Some(Expression::new_binary_expression(
span,
// see [`PeepholeOptimizations::is_binary_operator_that_does_number_conversion`] why it does not require `wrap_with_unary_plus_if_needed` here
first_arg.take_in(ctx),
first_arg,
BinaryOperator::Exponential,
wrap_with_unary_plus_if_needed(second_arg),
ctx,
@@ -341,22 +341,18 @@ impl<'a> PeepholeOptimizations {
if right.operator != e.operator {
return;
}
let Expression::LogicalExpression(mut right) = e.right.take_in(ctx) else { return };
let Expression::LogicalExpression(right) = e.right.take_in(ctx) else { return };
let LogicalExpression { left: right_left, right: right_right, .. } = right.unbox();
let mut new_left = Expression::new_logical_expression(
e.span,
e.left.take_in(ctx),
e.operator,
right.left.take_in(ctx),
right_left,
ctx,
);
Self::substitute_rotate_logical_expression(&mut new_left, ctx);
let new_value = Expression::new_logical_expression(
e.span,
new_left,
e.operator,
right.right.take_in(ctx),
ctx,
);
let new_value =
Expression::new_logical_expression(e.span, new_left, e.operator, right_right, ctx);
ctx.replace_expression(expr, new_value);
}
@@ -382,24 +378,20 @@ impl<'a> PeepholeOptimizations {
&& right.operator == e.operator
&& !right.right.may_have_side_effects(ctx)
{
let Expression::BinaryExpression(mut right) = e.right.take_in(ctx) else {
let Expression::BinaryExpression(right) = e.right.take_in(ctx) else {
return;
};
let BinaryExpression { left: right_left, right: right_right, .. } = right.unbox();
let mut new_left = Expression::new_binary_expression(
e.span,
e.left.take_in(ctx),
e.operator,
right.left.take_in(ctx),
right_left,
ctx,
);
Self::substitute_rotate_binary_expression(&mut new_left, ctx);
let new_value = Expression::new_binary_expression(
e.span,
new_left,
e.operator,
right.right.take_in(ctx),
ctx,
);
let new_value =
Expression::new_binary_expression(e.span, new_left, e.operator, right_right, ctx);
ctx.replace_expression(expr, new_value);
return;
}
@@ -1471,31 +1463,25 @@ impl<'a> PeepholeOptimizations {
let new_args = args;
for arg in old_args {
if let Argument::SpreadElement(mut spread_el) = arg {
if let Expression::ArrayExpression(array_expr) = &mut spread_el.argument {
for el in &mut array_expr.elements {
if let Argument::SpreadElement(spread_el) = arg {
let SpreadElement { span, argument, .. } = spread_el.unbox();
if let Expression::ArrayExpression(array_expr) = argument {
for el in array_expr.unbox().elements {
match el {
ArrayExpressionElement::SpreadElement(spread_el) => {
new_args.push(Argument::new_spread_element(
spread_el.span,
spread_el.argument.take_in(ctx),
ctx,
));
let SpreadElement { span, argument, .. } = spread_el.unbox();
new_args.push(Argument::new_spread_element(span, argument, ctx));
}
ArrayExpressionElement::Elision(elision) => {
new_args.push(Expression::new_void_0(elision.span, ctx).into());
}
match_expression!(ArrayExpressionElement) => {
new_args.push(el.to_expression_mut().take_in(ctx).into());
new_args.push(el.into_expression().into());
}
}
}
} else {
new_args.push(Argument::new_spread_element(
spread_el.span,
spread_el.argument.take_in(ctx),
ctx,
));
new_args.push(Argument::new_spread_element(span, argument, ctx));
}
} else {
new_args.push(arg);
@@ -5,9 +5,9 @@ checker.ts:
sys deallocs: 150
sys alloc bytes: 14996600 # 15.00 MB
sys peak growth: 10169560 # 10.17 MB
arena allocs: 148403
arena allocs: 145315
arena reallocs: 28468
arena size: 19204712 # 19.20 MB
arena size: 19154792 # 19.15 MB
App.tsx:
file size: 415342 # 415.34 kB
@@ -16,9 +16,9 @@ App.tsx:
sys deallocs: 61
sys alloc bytes: 2128555 # 2.13 MB
sys peak growth: 1612427 # 1.61 MB
arena allocs: 16038
arena allocs: 15795
arena reallocs: 2721
arena size: 2887488 # 2.89 MB
arena size: 2883328 # 2.88 MB
RadixUIAdoptionSection.jsx:
file size: 2520 # 2.52 kB
@@ -27,7 +27,7 @@ RadixUIAdoptionSection.jsx:
sys deallocs: 21
sys alloc bytes: 17400 # 17.40 kB
sys peak growth: 11628 # 11.63 kB
arena allocs: 30
arena allocs: 29
arena reallocs: 6
arena size: 35632 # 35.63 kB
@@ -38,9 +38,9 @@ pdf.mjs:
sys deallocs: 2427
sys alloc bytes: 5334247 # 5.33 MB
sys peak growth: 3693355 # 3.69 MB
arena allocs: 45817
arena allocs: 45153
arena reallocs: 7718
arena size: 6326224 # 6.33 MB
arena size: 6314208 # 6.31 MB
antd.js:
file size: 6686316 # 6.69 MB
@@ -49,9 +49,9 @@ antd.js:
sys deallocs: 1038
sys alloc bytes: 29976031 # 29.98 MB
sys peak growth: 19934725 # 19.93 MB
arena allocs: 371595
arena allocs: 353569
arena reallocs: 76289
arena size: 49108808 # 49.11 MB
arena size: 48820232 # 48.82 MB
binder.ts:
file size: 193077 # 193.08 kB
@@ -60,9 +60,9 @@ binder.ts:
sys deallocs: 33
sys alloc bytes: 963008 # 963.01 kB
sys peak growth: 657404 # 657.40 kB
arena allocs: 7030
arena allocs: 6877
arena reallocs: 842
arena size: 1167360 # 1.17 MB
arena size: 1164880 # 1.16 MB
kitchen-sink.tsx:
file size: 732898 # 732.90 kB
@@ -71,6 +71,6 @@ kitchen-sink.tsx:
sys deallocs: 1854
sys alloc bytes: 5307512 # 5.31 MB
sys peak growth: 3707425 # 3.71 MB
arena allocs: 66752
arena allocs: 65970
arena reallocs: 12252
arena size: 9424360 # 9.42 MB
arena size: 9410968 # 9.41 MB