From 85c8c600ce7f986afee154c4b8bb9c605a3ca0b4 Mon Sep 17 00:00:00 2001 From: Hamidreza Hanafi Date: Tue, 28 Jul 2026 17:51:05 -0400 Subject: [PATCH] perf(es/minifier): Apply pending substitutions in batch for if-dense statement lists The optimizer flushed pending substitutions into each `if` statement individually before `merge_similar_ifs` (one Finalizer + one NormalMultiReplacer walk per `if`). On typescript.js this produced ~68k double walks per optimize() call with 0.4-1.7% hit rates. Apply the substitutions to the whole statement list in a single batch walk when the list is if-dense (>= 16 ifs), and keep the per-if loop otherwise. Both forms are semantically equivalent: the substitution maps only mutate via consumption of applied entries, which happens in the same statement order either way. es/minifier/libs benchmarks: 2.5-13.8% faster across all 12 lib fixtures (typescript -13.8%, echarts -11.8%, lodash -10.5%), byte-identical output on the entire fixture corpus. --- .changeset/batch-substitutions-if-dense.md | 6 ++++ .../src/compress/optimize/conditionals.rs | 11 +++---- .../src/compress/optimize/mod.rs | 29 +++++++++++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 .changeset/batch-substitutions-if-dense.md diff --git a/.changeset/batch-substitutions-if-dense.md b/.changeset/batch-substitutions-if-dense.md new file mode 100644 index 000000000000..47d03ece72d5 --- /dev/null +++ b/.changeset/batch-substitutions-if-dense.md @@ -0,0 +1,6 @@ +--- +swc_core: patch +swc_ecma_minifier: patch +--- + +perf(es/minifier): Apply pending substitutions in batch for if-dense statement lists diff --git a/crates/swc_ecma_minifier/src/compress/optimize/conditionals.rs b/crates/swc_ecma_minifier/src/compress/optimize/conditionals.rs index 4f6c6d4c1499..d7a7bd3f9a33 100644 --- a/crates/swc_ecma_minifier/src/compress/optimize/conditionals.rs +++ b/crates/swc_ecma_minifier/src/compress/optimize/conditionals.rs @@ -251,13 +251,10 @@ impl Optimizer<'_> { return; } - // we must inline first to avoid https://github.com/swc-project/swc/issues/11517 - stmts - .iter_mut() - .filter_map(|s| s.as_stmt_mut().and_then(|s| s.as_mut_if_stmt())) - .for_each(|s| { - self.changed |= self.vars.inline_with_multi_replacer(s); - }); + // NOTE: The caller is responsible for applying pending substitutions + // to the statement list (see `handle_stmt_likes`, which does it in a + // single batch walk for `if`-dense lists) before merging. This is + // required to avoid https://github.com/swc-project/swc/issues/11517 let has_work = stmts diff --git a/crates/swc_ecma_minifier/src/compress/optimize/mod.rs b/crates/swc_ecma_minifier/src/compress/optimize/mod.rs index a65af9d81d58..6a53793a8031 100644 --- a/crates/swc_ecma_minifier/src/compress/optimize/mod.rs +++ b/crates/swc_ecma_minifier/src/compress/optimize/mod.rs @@ -463,6 +463,8 @@ impl Optimizer<'_> { where T: StmtLike + ModuleItemLike + ModuleItemExt + VisitMutWith + VisitWith, Vec: VisitMutWith + VisitWith> + VisitWith, + Vec: + for<'aa> VisitMutWith> + for<'aa> VisitMutWith>, { let mut use_asm = false; let prepend_stmts = self.prepend_stmts.take(); @@ -546,6 +548,33 @@ impl Optimizer<'_> { stmts.visit_with(&mut AssertValid); } + // Apply pending substitutions (required before `merge_similar_ifs` to + // avoid https://github.com/swc-project/swc/issues/11517). Strategy + // depends on `if` density: for `if`-dense lists a single batch walk + // beats two walks per `if`; for `if`-sparse lists the per-`if` walks + // stay cheaper than walking the whole list twice. Both are + // semantically equivalent: the substitution maps do not change during + // application except for consumption of applied entries, which happens + // in the same statement order either way. + let if_count = stmts + .iter() + .filter(|s| { + s.as_stmt() + .map(|s| matches!(s, Stmt::If(_))) + .unwrap_or(false) + }) + .count(); + if if_count >= 16 { + self.changed |= self.vars.inline_with_multi_replacer(stmts); + } else { + stmts + .iter_mut() + .filter_map(|s| s.as_stmt_mut().and_then(|s| s.as_mut_if_stmt())) + .for_each(|s| { + self.changed |= self.vars.inline_with_multi_replacer(s); + }); + } + self.merge_similar_ifs(stmts); #[cfg(debug_assertions)]