feat(minifier): expand switch optimiation to remove empty cases (#24520)

remove empty cases:
- before default or at end (if no default).
- only if they don't affect behavior.

refactored implementation from #17523 

ref #17544

---------

Co-authored-by: Dunqing <dengqing0821@gmail.com>
This commit is contained in:
ArmanoandDunqing authored and GitHub committed 2026-07-16 10:36:03 +08:00
1 parent 1e50a2d6a0
commit 3acf4c1789
7 files changed
+105 -47

No files matched your search

@@ -722,6 +722,22 @@ impl<'a> PeepholeOptimizations {
false
}
fn is_switch_case_removable(stmt: &SwitchCase, allow_break: bool) -> bool {
let is_empty = if stmt.consequent.len() == 1 {
match stmt.consequent.last() {
Some(Statement::EmptyStatement(_)) => true,
Some(Statement::BreakStatement(break_stmt)) => {
allow_break && break_stmt.label.is_none()
}
_ => false,
}
} else {
stmt.consequent.is_empty()
};
is_empty && stmt.test.as_ref().is_none_or(Expression::is_literal)
}
fn handle_switch_statement(
mut switch_stmt: ArenaBox<'a, SwitchStatement<'a>>,
result: &mut ArenaVec<'a, Statement<'a>>,
@@ -745,33 +761,56 @@ impl<'a> PeepholeOptimizations {
ctx.drop_statement(&dropped);
}
// Prune empty case clauses before a trailing default
// e.g., `switch (x) { case 0: foo(); break; case 1: default: bar() }`
// => `switch (x) { case 0: foo(); break; default: bar() }`
// Remove empty case clauses that don't affect behavior.
// Handles fall-through semantics: remove empty cases before default or at end (if no default).
// e.g., `switch(x){ case 0: foo(); break; case 1: default: bar() }`
// => `switch(x){ case 0: foo(); break; default: bar() }`
// https://github.com/evanw/esbuild/commit/add452ed51333953dd38a26f28a775bb220ea2e9
if let Some(last_case) = switch_stmt.cases.last()
&& last_case.test.is_none()
{
let default_idx = switch_stmt.cases.len() - 1;
let mut first_empty_idx = default_idx;
// Iterate backward through preceding cases
while first_empty_idx > 0 {
let case = &switch_stmt.cases[first_empty_idx - 1];
// Only remove empty cases with primitive literal tests
if case.consequent.is_empty()
&& case.test.as_ref().is_some_and(Expression::is_literal)
{
first_empty_idx -= 1;
} else {
break;
}
let case_count = switch_stmt.cases.len();
if case_count == 1 {
// Remove sole case if empty and has no side-effect test
if Self::is_switch_case_removable(&switch_stmt.cases[0], true) {
ctx.drop_switch_case(&switch_stmt.cases.pop().unwrap());
}
// If we found cases to remove, keep cases [0..first_empty_idx] + [default]
if first_empty_idx < default_idx {
let default_case = switch_stmt.cases.pop().unwrap();
switch_stmt.cases.truncate(first_empty_idx);
switch_stmt.cases.push(default_case);
ctx.notice_change();
} else if case_count > 1 {
// Determine the range [0, end] to check for removable cases.
// 1. default exists and is empty: check the full switch.
// 2. default exists and is non-removable and last: check only cases before that default.
// 3. default exists, is non-removable, and is not last: skip this optimization (`end = 0`).
// 4. no default case: check the full switch and allow a trailing unlabeled `break`.
let default_pos = switch_stmt.cases.iter().rposition(SwitchCase::is_default_case);
let (end, allow_break) = if let Some(default_pos) = default_pos {
if Self::is_switch_case_removable(&switch_stmt.cases[default_pos], true) {
(case_count, true)
} else if default_pos == case_count - 1 {
(default_pos, false)
} else {
(0, false)
}
} else {
(case_count, true)
};
if end > 0 {
// Last non-removable case index in [0, end]. Returns None if all cases are removable.
let last_non_removable_case_before_end = switch_stmt.cases[..end]
.iter()
.rposition(|case| !Self::is_switch_case_removable(case, allow_break));
// Calculate the start of the removable suffix.
// 1. next case after last non-removable: remove from pos + 1
// 2. no non-removable case: all cases are removable, start from 0
let start = match last_non_removable_case_before_end {
Some(pos) => pos + 1,
None => 0,
};
// Remove the removable suffix if any
if start < end && default_pos.is_none_or(|pos| pos >= start) {
for removed_case in switch_stmt.cases.drain(start..end) {
ctx.drop_switch_case(&removed_case);
}
}
}
}
@@ -522,4 +522,14 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> {
self.dirty_diff().visit_variable_declarator(decl);
self.state.record_mutation();
}
/// Mark a switch case subtree as about to be dropped. Walks the entire case —
/// test expression (if present) and all statements in the consequent. Same contract
/// as `drop_expression`. Use this helper when removing a case from a switch statement's
/// case vector to properly notify the scope tracking system about dropped references.
#[inline]
pub fn drop_switch_case(&mut self, switch_case: &SwitchCase<'a>) {
self.dirty_diff().visit_switch_case(switch_case);
self.state.record_mutation();
}
}
@@ -2425,5 +2425,5 @@ fn prune_empty_case_before_default() {
test("switch (x) { default: bar() }", "switch (x) { default: bar();}");
// No default - nothing to prune
test("switch (x) { case 0: foo(); case 1: }", "switch (x) { case 0: foo(); case 1:}");
test("switch (x) { case 0: foo(); case 1: }", "switch (x) { case 0: foo(); }");
}
@@ -29,7 +29,7 @@ fn test_fold_one_child_blocks() {
test_same("function f(){try{foo()}catch(e){bar(e)}finally{baz()}}");
// Try it out with switch statements
test_same("function f(){switch(x){case 1:break}}");
test("function f(){switch(x){case 1:break}}", "function f(){x;}");
// Do while loops stay in a block if that's where they started
test(
@@ -126,46 +126,50 @@ fn test_object_property_shorthand_normalisation_skips_proto_setter() {
#[test]
fn test_handle_switch_statement() {
test("switch (a()){}", "a()"); // a()
test_same("switch (a) { default: }"); // a;
test_same("switch (a) { default: break;}"); // a;
test("switch (a()) {}", "a()");
test("switch (a) { default: }", "a;");
test("switch (a) { default: break;}", " a;");
test_same("switch (a) { default: var b; break;}"); // a; var b;
test_same("switch (a) { default: b()}"); // a; b();
test_same("switch (a) { default: b(); return;}"); // a, b(); return;
test_same("switch (a) { case 1: break;}"); // a;
test("switch (a) { case 1: break;}", "a;");
test_same("switch (a) { case 1: b();}"); // a === 1 && b();
test_same("switch (a) { case 1: b();break; }"); // a === 1 && b();
test_same("switch (a) { case 1: b();return; }"); // if (a === 1) { b(); return; }
test_same("switch (a) { default: case 1: }"); // a;
test("switch (a) { case 1: default: }", "switch (a) { default:}"); // a;
test("switch (a) { default: case 1: }", "a;");
test("switch (a) { case 1: default: }", "a;");
test_same("switch (a) { case 1: default: break; case 2: b()}");
test_same("switch (a) { case 1: b(); default: c()}"); // a === 1 && b(); c();
test_same("switch (a) { case 1: default: b(); case 2: c();}");
test_same("switch (a) { case 1: b(); default: break; case 2: c()}");
test_same("switch (a) { case 1: b(); case 2: break; case 3: c()}");
test_same("switch (a) { case 1: b(); break; case 2: c();break;}");
test_same("switch (x) { default: foo(); case 1: }");
test_same("switch (a) { case 1: b(); case 2: b();}");
test_same("switch (a) { case 1: case 2: b(); }");
test_same("switch (a) { case 1: var c=2; break;}"); // if (a === 1) { var c=2; }
test(
"switch (a) { case 1: case 2: default: b(); break;}",
"switch (a) { default: b(); break;}",
); // a, b();
test_same("switch (a) { default: break; case 1: break;}"); // a;
test("switch (a) { default: break; case 1: break;}", "a;");
test_same("switch (a) { default: b();break;case 1: c();break;}"); // a === 1 ? c() : b();
test(
"switch (a) { default: {b();break;} case 1: {c();break;}}",
"switch (a) { default: b();break;case 1: c();break;}",
); // a === 1 ? c() : b();
test_same("switch (a) { case b(): default:}"); // a, b();
test_same("switch (a) { case 2: case 1: break; default: break;}"); // a;
test_same("switch (a) { case 3: b(); break; case 2: break;}"); // a === 3 && b();
test_same("switch (a) { case 3: b(); case 2: break;}"); // a === 3 && b();
test("switch (a) { case b(): default:}", "switch (a) { case b(): }"); // a, b();
test("switch (a) { case 2: case 1: break; default: break;}", "a;");
test("switch (a) { case 3: b(); break; case 2: break;}", "switch (a) { case 3: b(); break; }"); // a === 3 && b();
test("switch (a) { case 3: b(); case 2: break;}", "switch (a) { case 3: b(); }"); // a === 3 && b();
test_same("switch (a) { case 3: b(); case 2: c(); break;}");
test_same("switch (a) { case 3: b(); case 2: case 1: break;}"); // a === 3 && b();
test_same("switch (a) { case 3: b(); case 2: case 1: }"); // a === 3 && b();
test("switch (a) { case 3: b(); case 2: case 1: break;}", "switch (a) { case 3: b(); }"); // a === 3 && b();
test("switch (a) { case 3: b(); case 2: case 1: }", "switch (a) { case 3: b(); }"); // a === 3 && b();
test_same("switch (x) { default: case 1: foo(); case 2: }"); // x !== 2 && foo();
test_same("switch (a) { case 3: if (b) break }"); // a === 3 && b;
test(
"switch (a) { case 3: { if(b) {c()} else {break;} }}",
@@ -189,7 +193,10 @@ fn test_handle_switch_statement() {
); // a === 1 && c(); b();
test_same("function f() { switch (a) { case 1: return;} }"); // function f() { a; }
test("switch (a()) { default: {let y;} }", "switch (a()) { default: { let y; } }"); // a(); { let y; }
test_same("function f(){switch ('x') { case 'x': var x = 1;break; case 'y': break; }}");
test(
"function f(){switch ('x') { case 'x': var x = 1;break; case 'y': break; }}",
"function f(){switch ('x') { case 'x': var x = 1;break; }}",
);
test("switch (a) { default: if(a) {break;}c();}", "switch (a) { default: if(a) break;c();}"); // a, !a && c();
test("switch (a) { case 1: if(a) {b();}c();}", "switch (a) { case 1: a && b(), c(); }"); // if (a === 1) { a && b(), c(); }
test("switch ('\\v') { case '\\u000B': foo();}", "switch ('\\v') { case '\\v': foo(); }"); // foo();
@@ -226,4 +233,6 @@ fn test_handle_switch_statement() {
test_same("switch (1) { case 2: var x=0;}"); // if (0) var x;
test_same("switch (b) { case 2: switch (a) { case 2: a();break;case 3: foo();break;}}"); // ;
test_same("switch (b) { case 2: switch (a) { case 2: foo()}}"); // if (b === 2 && a === 2) foo()
test_same("function f(){ switch (0) { case x: break; } let x = 1; }"); // TDZ
}
+2 -2
View File
@@ -21,7 +21,7 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
3.20 MB | 1.00 MB | 1.01 MB | 322.59 kB | 331.56 kB | 2 | echarts.js
6.69 MB | 2.13 MB | 2.31 MB | 455.50 kB | 488.28 kB | 5 | antd.js
6.69 MB | 2.13 MB | 2.31 MB | 455.48 kB | 488.28 kB | 5 | antd.js
10.95 MB | 3.33 MB | 3.49 MB | 853.06 kB | 915.50 kB | 4 | typescript.js
10.95 MB | 3.33 MB | 3.49 MB | 853.03 kB | 915.50 kB | 4 | typescript.js
@@ -26,7 +26,7 @@ pdf.mjs:
file size: 567296 # 567.30 kB
sys allocs: 2469
sys reallocs: 205
arena allocs: 47471
arena allocs: 47467
arena reallocs: 7742
arena size: 6356528 # 6.36 MB
@@ -34,9 +34,9 @@ antd.js:
file size: 6686316 # 6.69 MB
sys allocs: 1044
sys reallocs: 56
arena allocs: 372632
arena allocs: 372552
arena reallocs: 76745
arena size: 49033120 # 49.03 MB
arena size: 49031840 # 49.03 MB
binder.ts:
file size: 193077 # 193.08 kB
@@ -50,7 +50,7 @@ kitchen-sink.tsx:
file size: 732898 # 732.90 kB
sys allocs: 2309
sys reallocs: 175
arena allocs: 88129
arena allocs: 88097
arena reallocs: 15654
arena size: 10190368 # 10.19 MB