Skip to content

perf(minifier): compute template-literal inline checks in a single pass#23467

Merged
Dunqing merged 1 commit into
oxc-project:mainfrom
hyf0:perf/minifier-template-literal-single-walk
Jun 16, 2026
Merged

perf(minifier): compute template-literal inline checks in a single pass#23467
Dunqing merged 1 commit into
oxc-project:mainfrom
hyf0:perf/minifier-template-literal-single-walk

Conversation

@hyf0

@hyf0 hyf0 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Measured allocation reduction

The change reduces allocations on the minifier hot path — deterministic, every delta a reduction (no
increases), captured in allocs_minifier.snap (updated in this PR):

file sys allocs: before → after arena allocs: before → after
kitchen-sink.tsx 2640 → 2638 (−2) 90554 → 90549 (−5)
App.tsx 115 → 113 (−2) 17012 → 17010 (−2)
pdf.mjs 2614 → 2613 (−1)
antd.js 337233 → 337232 (−1)

(Small in absolute terms, but a clean deterministic reduction with zero regressions across every file.)

What & why

inline_template_literal ran its may_have_side_effects + to_js_string checks twice per
expression — once in an .any() pre-scan, then again in the drain loop that partitioned the
expressions. This collapses them into a single pass, so per expression the checks go from ≈2 to
exactly 1 (never more in any case), and the rebuild is sized exactly:

let mut inline_exprs = Vec::new();
for (idx, expr) in t.expressions.iter().enumerate() {
    if !expr.may_have_side_effects(ctx) && let Some(str) = expr.to_js_string(ctx) {
        inline_exprs.push((idx, str));
    }
}
if inline_exprs.is_empty() { return; }
let mut kept = ctx.ast.vec_with_capacity(t.expressions.len() - inline_exprs.len());
let mut inline_idxs = inline_exprs.iter().map(|(idx, _)| *idx).peekable();
for (idx, expr) in t.expressions.drain(..).enumerate() {
    if inline_idxs.peek() == Some(&idx) { inline_idxs.next(); ctx.drop_expression(&expr); }
    else { kept.push(expr); }
}
t.expressions = kept;

to_js_string recurses over the expression and can allocate, so removing the duplicate calls is real
work. inline_exprs also moves from Vec::with_capacity(len) (#20389) to a lazy Vec::new() (no
allocation on the common no-inline path), and kept is sized exactly (len - inline_exprs.len()) —
which is what produces the allocation reduction above.

Behavior-preserving

Computing all to_js_string values before any drop_expression is safe: drop_expression only sets
dirty/mutation bookkeeping, disjoint from the immutable scoping/symbol state the checks read. Indices
are collected ascending, so the index-matched rebuild reproduces the exact same kept / dropped /
inlined partition. cargo test -p oxc_minifier (535 tests) passes with no snapshot drift.

CodSpeed will arbitrate the instruction-count effect of the de-duplicated checks (constant template
interpolations that trigger inlining are uncommon in real code, so that delta may be small); the
allocation reduction above is the deterministic part.


Prepared with AI assistance.

@hyf0 hyf0 force-pushed the perf/minifier-template-literal-single-walk branch from a00f4e3 to 967e54f Compare June 16, 2026 03:48
@codspeed-hq

codspeed-hq Bot commented Jun 16, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 52 untouched benchmarks
⏩ 19 skipped benchmarks1


Comparing hyf0:perf/minifier-template-literal-single-walk (ffb8240) with main (3699971)2

Open in CodSpeed

Footnotes

  1. 19 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (3170c0e) during the generation of this report, so 3699971 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@hyf0 hyf0 marked this pull request as ready for review June 16, 2026 05:05
@camc314 camc314 added the A-minifier Area - Minifier label Jun 16, 2026
@hyf0 hyf0 force-pushed the perf/minifier-template-literal-single-walk branch from 967e54f to ffb8240 Compare June 16, 2026 11:40
@Dunqing Dunqing added the 0-merge Merge with Graphite Merge Queue label Jun 16, 2026

Dunqing commented Jun 16, 2026

Copy link
Copy Markdown
Member

Merge activity

  • Jun 16, 3:20 PM UTC: The merge label '0-merge' was detected. This PR will be added to the Graphite merge queue once it meets the requirements.
  • Jun 16, 3:20 PM UTC: Dunqing added this pull request to the Graphite merge queue.

@Dunqing Dunqing merged commit 970e09a into oxc-project:main Jun 16, 2026
38 checks passed
Boshen added a commit that referenced this pull request Jun 18, 2026
### 💥 BREAKING CHANGES

- 7a76cd3 estree: [**BREAKING**] Make whether to include TS fields a
runtime option (#23574) (overlookmotel)
- e7b6b68 estree: [**BREAKING**] `ESTree` config use methods not consts
(#23573) (overlookmotel)

### 🚀 Features

- 556cc6d data_structures: Add `CodeBuffer::as_str` method (#23571)
(overlookmotel)
- 38c4b06 parser: Add friendly error for adjacent JSX elements (#23378)
(sapphi-red)
- 53509a8 minifier: Treeshake pure typed arrays and Set/Map array
literals (#23469) (Dunqing)
- 09762d9 minifier: Inline const value for read-only vars (#22593)
(Dunqing)

### 🐛 Bug Fixes

- 20375f9 react_compiler: Keep imports referenced only by a computed key
(#23586) (Boshen)
- 31bfd9b minifier: Keep Object introspection calls on a possible Proxy
(#23483) (Dunqing)
- 837a395 parser: Treat a line comment after ':' as leading, not
trailing (#23515) (Dunqing)
- e409fe0 minifier: Keep `new Map`/`WeakSet`/`WeakMap` with a string
argument (#23470) (Dunqing)
- ae02b4e ci/parser: Use `minimal` for vitest reporter (#23457)
(camc314)

### ⚡ Performance

- cf24329 mangler: Compile slot sort once instead of per CAPACITY
(#23577) (Boshen)
- 4058a6a parser: Reduce code bloat from verify_modifiers
monomorphization (#23576) (Boshen)
- 053b0c1 estree: Remove pointless `mem::take` (#23572) (overlookmotel)
- dfb52b6 transformer: Pre-size statement vecs in TS enum & namespace
lowering (#23516) (Yunfei He)
- 970e09a minifier: Compute template-literal inline checks in a single
pass (#23467) (Yunfei He)
- 3170c0e semantic,mangler,minifier: Fix `Semantic::stats` node count
and reuse stats in mangler builds (#23352) (Boshen)
- d1fa6e0 minifier: Evaluate ternary branches once in
minimize_conditional_expression (#23479) (Yunfei He)
- 3fa8051 transformer: Pre-size JSX props vec to attribute count
(#23466) (Yunfei He)
- 488b382 react_compiler: Borrow binding names in prefilter instead of
allocating (#23471) (Yunfei He)
- bcb3894 minifier: Incremental scoping refresh, delete
LiveUsageCollector (#23197) (Dunqing)

### 📚 Documentation

- f68641e data_structures: Improve docs on safety contract (#23575)
(overlookmotel)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
camc314 pushed a commit that referenced this pull request Jul 3, 2026
…ss (#23467)

## Measured allocation reduction

The change reduces allocations on the minifier hot path — deterministic,
every delta a reduction (no
increases), captured in `allocs_minifier.snap` (updated in this PR):

| file | sys allocs: before → after | arena allocs: before → after |
| --- | --- | --- |
| kitchen-sink.tsx | 2640 → 2638 (**−2**) | 90554 → 90549 (**−5**) |
| App.tsx | 115 → 113 (**−2**) | 17012 → 17010 (**−2**) |
| pdf.mjs | 2614 → 2613 (**−1**) | — |
| antd.js | — | 337233 → 337232 (**−1**) |

(Small in absolute terms, but a clean deterministic reduction with zero
regressions across every file.)

## What & why

`inline_template_literal` ran its `may_have_side_effects` +
`to_js_string` checks **twice** per
expression — once in an `.any()` pre-scan, then again in the drain loop
that partitioned the
expressions. This collapses them into a **single pass**, so per
expression the checks go from ≈2 to
exactly 1 (never more in any case), and the rebuild is sized exactly:

```rust
let mut inline_exprs = Vec::new();
for (idx, expr) in t.expressions.iter().enumerate() {
    if !expr.may_have_side_effects(ctx) && let Some(str) = expr.to_js_string(ctx) {
        inline_exprs.push((idx, str));
    }
}
if inline_exprs.is_empty() { return; }
let mut kept = ctx.ast.vec_with_capacity(t.expressions.len() - inline_exprs.len());
let mut inline_idxs = inline_exprs.iter().map(|(idx, _)| *idx).peekable();
for (idx, expr) in t.expressions.drain(..).enumerate() {
    if inline_idxs.peek() == Some(&idx) { inline_idxs.next(); ctx.drop_expression(&expr); }
    else { kept.push(expr); }
}
t.expressions = kept;
```

`to_js_string` recurses over the expression and can allocate, so
removing the duplicate calls is real
work. `inline_exprs` also moves from `Vec::with_capacity(len)` (#20389)
to a lazy `Vec::new()` (no
allocation on the common no-inline path), and `kept` is sized exactly
(`len - inline_exprs.len()`) —
which is what produces the allocation reduction above.

## Behavior-preserving

Computing all `to_js_string` values before any `drop_expression` is
safe: `drop_expression` only sets
dirty/mutation bookkeeping, disjoint from the immutable scoping/symbol
state the checks read. Indices
are collected ascending, so the index-matched rebuild reproduces the
exact same kept / dropped /
inlined partition. `cargo test -p oxc_minifier` (535 tests) passes with
no snapshot drift.

CodSpeed will arbitrate the instruction-count effect of the
de-duplicated checks (constant template
interpolations that trigger inlining are uncommon in real code, so that
delta may be small); the
allocation reduction above is the deterministic part.

---

Prepared with AI assistance.
camc314 pushed a commit that referenced this pull request Jul 3, 2026
### 💥 BREAKING CHANGES

- 7a76cd3 estree: [**BREAKING**] Make whether to include TS fields a
runtime option (#23574) (overlookmotel)
- e7b6b68 estree: [**BREAKING**] `ESTree` config use methods not consts
(#23573) (overlookmotel)

### 🚀 Features

- 556cc6d data_structures: Add `CodeBuffer::as_str` method (#23571)
(overlookmotel)
- 38c4b06 parser: Add friendly error for adjacent JSX elements (#23378)
(sapphi-red)
- 53509a8 minifier: Treeshake pure typed arrays and Set/Map array
literals (#23469) (Dunqing)
- 09762d9 minifier: Inline const value for read-only vars (#22593)
(Dunqing)

### 🐛 Bug Fixes

- 20375f9 react_compiler: Keep imports referenced only by a computed key
(#23586) (Boshen)
- 31bfd9b minifier: Keep Object introspection calls on a possible Proxy
(#23483) (Dunqing)
- 837a395 parser: Treat a line comment after ':' as leading, not
trailing (#23515) (Dunqing)
- e409fe0 minifier: Keep `new Map`/`WeakSet`/`WeakMap` with a string
argument (#23470) (Dunqing)
- ae02b4e ci/parser: Use `minimal` for vitest reporter (#23457)
(camc314)

### ⚡ Performance

- cf24329 mangler: Compile slot sort once instead of per CAPACITY
(#23577) (Boshen)
- 4058a6a parser: Reduce code bloat from verify_modifiers
monomorphization (#23576) (Boshen)
- 053b0c1 estree: Remove pointless `mem::take` (#23572) (overlookmotel)
- dfb52b6 transformer: Pre-size statement vecs in TS enum & namespace
lowering (#23516) (Yunfei He)
- 970e09a minifier: Compute template-literal inline checks in a single
pass (#23467) (Yunfei He)
- 3170c0e semantic,mangler,minifier: Fix `Semantic::stats` node count
and reuse stats in mangler builds (#23352) (Boshen)
- d1fa6e0 minifier: Evaluate ternary branches once in
minimize_conditional_expression (#23479) (Yunfei He)
- 3fa8051 transformer: Pre-size JSX props vec to attribute count
(#23466) (Yunfei He)
- 488b382 react_compiler: Borrow binding names in prefilter instead of
allocating (#23471) (Yunfei He)
- bcb3894 minifier: Incremental scoping refresh, delete
LiveUsageCollector (#23197) (Dunqing)

### 📚 Documentation

- f68641e data_structures: Improve docs on safety contract (#23575)
(overlookmotel)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

0-merge Merge with Graphite Merge Queue A-minifier Area - Minifier

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants