Skip to content

Commit

Permalink
feat(minifier): implement collapse-variable-declarations (#6464)
Browse files Browse the repository at this point in the history
  • Loading branch information
shulaoda authored Oct 13, 2024
1 parent f960e9e commit 97c8a36
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 66 deletions.
106 changes: 55 additions & 51 deletions crates/oxc_minifier/src/ast_passes/collapse_variable_declarations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,65 +36,75 @@ impl<'a> CollapseVariableDeclarations {
Self { options, changed: false }
}

/// Join consecutive var statements
fn is_require_call(var_decl: &VariableDeclaration) -> bool {
var_decl
.declarations
.first()
.and_then(|d| d.init.as_ref())
.is_some_and(Expression::is_require_call)
}

fn is_valid_var_decl(
stmt: &Statement,
kind: Option<VariableDeclarationKind>,
) -> Option<VariableDeclarationKind> {
if let Statement::VariableDeclaration(cur_decl) = stmt {
let is_not_require_call = !Self::is_require_call(cur_decl);
if kind.map_or(true, |k| cur_decl.kind == k) && is_not_require_call {
return Some(cur_decl.kind);
}
}
None
}

fn join_vars(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
if self.options.join_vars {
if !self.options.join_vars || stmts.len() < 2 {
return;
}
// Collect all the consecutive ranges that contain joinable vars.
// This is required because Rust prevents in-place vec mutation.
let mut ranges = vec![];
let mut range = 0..0;
let mut i = 1usize;
let mut capacity = 0usize;
for window in stmts.windows(2) {
let [prev, cur] = window else { unreachable!() };
if let (
Statement::VariableDeclaration(cur_decl),
Statement::VariableDeclaration(prev_decl),
) = (cur, prev)

let mut prev: usize = stmts.len() - 1;
let mut items = std::vec::Vec::<usize>::new();

while prev > 0 {
prev -= 1;

let cur: usize = prev + 1;

if !Self::is_valid_var_decl(&stmts[cur], None)
.is_some_and(|kind| Self::is_valid_var_decl(&stmts[prev], Some(kind)).is_some())
{
// Do not join `require` calls for cjs-module-lexer.
if cur_decl
.declarations
.first()
.and_then(|d| d.init.as_ref())
.is_some_and(Expression::is_require_call)
{
break;
}
if cur_decl.kind == prev_decl.kind {
if i - 1 != range.end {
range.start = i - 1;
}
range.end = i + 1;
}
continue;
}
if (range.end != i || i == stmts.len() - 1) && range.start < range.end {
capacity += range.end - range.start - 1;
ranges.push(range.clone());
range = 0..0;
let Some(Statement::VariableDeclaration(cur_decl)) = stmts.get_mut(cur) else {
continue;
};

let mut decls = ctx.ast.move_vec(&mut cur_decl.declarations);
if let Some(Statement::VariableDeclaration(prev_decl)) = stmts.get_mut(prev) {
items.push(cur);
prev_decl.declarations.append(&mut decls);
}
i += 1;
}

if ranges.is_empty() {
if items.is_empty() {
return;
}

// Reconstruct the stmts array by joining consecutive ranges
let mut new_stmts = ctx.ast.vec_with_capacity(stmts.len() - capacity);
for (i, stmt) in stmts.drain(..).enumerate() {
if i > 0 && ranges.iter().any(|range| range.contains(&(i - 1)) && range.contains(&i)) {
if let Statement::VariableDeclaration(prev_decl) = new_stmts.last_mut().unwrap() {
if let Statement::VariableDeclaration(mut cur_decl) = stmt {
prev_decl.declarations.append(&mut cur_decl.declarations);
}
let mut item_iter = items.iter().rev();
let mut next_item = item_iter.next();

let mut new_stmts = ctx.ast.vec_with_capacity(stmts.len() - items.len());

for (index, stmt) in stmts.drain(..).enumerate() {
if let Some(item) = next_item {
if *item == index {
next_item = item_iter.next();
continue;
}
} else {
new_stmts.push(stmt);
}
new_stmts.push(stmt);
}

*stmts = new_stmts;
self.changed = true;
}
Expand Down Expand Up @@ -131,7 +141,6 @@ mod test {
}

#[test]
#[ignore]
fn test_collapsing() {
// Basic collapsing
test("var a;var b;", "var a,b;");
Expand Down Expand Up @@ -177,7 +186,6 @@ mod test {
}

#[test]
#[ignore]
fn test_issue397() {
test_same("var x; x = 5; var z = 7;");
test("var x; var y = 3; x = 5;", "var x, y = 3; x = 5;");
Expand All @@ -192,7 +200,6 @@ mod test {

// ES6 Tests
#[test]
#[ignore]
fn test_collapsing_let_const() {
// Basic collapsing
test("let a;let b;", "let a,b;");
Expand Down Expand Up @@ -229,7 +236,6 @@ mod test {
}

#[test]
#[ignore]
fn test_redeclaration_let_in_function() {
test(
"function f() { let x = 1; let y = 2; let z = 3; x + y + z; }",
Expand All @@ -248,7 +254,6 @@ mod test {
}

#[test]
#[ignore]
fn test_arrow_function() {
test("() => {let x = 1; let y = 2; x + y; }", "() => {let x = 1, y = 2; x + y; }");

Expand All @@ -264,7 +269,6 @@ mod test {
}

#[test]
#[ignore]
fn test_mixed_declaration_types() {
// lets, vars, const declarations consecutive
test("let x = 1; let z = 3; var y = 2;", "let x = 1, z = 3; var y = 2;");
Expand Down
7 changes: 4 additions & 3 deletions crates/oxc_minifier/src/compressor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ impl<'a> Compressor<'a> {
return;
}

ExploitAssigns::new().build(program, &mut ctx);
CollapseVariableDeclarations::new(self.options).build(program, &mut ctx);

// See `latePeepholeOptimizations`
let mut passes: [&mut dyn CompressorPass; 6] = [
&mut StatementFusion::new(),
Expand Down Expand Up @@ -74,6 +71,10 @@ impl<'a> Compressor<'a> {
}
i += 1;
}

// Passes listed in `getFinalization` in `DefaultPassConfig`
ExploitAssigns::new().build(program, &mut ctx);
CollapseVariableDeclarations::new(self.options).build(program, &mut ctx);
}

fn dead_code_elimination(program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
Expand Down
24 changes: 12 additions & 12 deletions tasks/minsize/minsize.snap
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
Original | Minified | esbuild | Gzip | esbuild

72.14 kB | 24.46 kB | 23.70 kB | 8.65 kB | 8.54 kB | react.development.js
72.14 kB | 24.12 kB | 23.70 kB | 8.62 kB | 8.54 kB | react.development.js

173.90 kB | 61.68 kB | 59.82 kB | 19.53 kB | 19.33 kB | moment.js
173.90 kB | 61.67 kB | 59.82 kB | 19.53 kB | 19.33 kB | moment.js

287.63 kB | 92.83 kB | 90.07 kB | 32.29 kB | 31.95 kB | jquery.js
287.63 kB | 92.70 kB | 90.07 kB | 32.27 kB | 31.95 kB | jquery.js

342.15 kB | 124.06 kB | 118.14 kB | 44.79 kB | 44.37 kB | vue.js
342.15 kB | 121.90 kB | 118.14 kB | 44.59 kB | 44.37 kB | vue.js

544.10 kB | 74.13 kB | 72.48 kB | 26.23 kB | 26.20 kB | lodash.js
544.10 kB | 73.49 kB | 72.48 kB | 26.13 kB | 26.20 kB | lodash.js

555.77 kB | 278.22 kB | 270.13 kB | 91.36 kB | 90.80 kB | d3.js
555.77 kB | 276.31 kB | 270.13 kB | 91.08 kB | 90.80 kB | d3.js

1.01 MB | 470.07 kB | 458.89 kB | 126.95 kB | 126.71 kB | bundle.min.js
1.01 MB | 467.63 kB | 458.89 kB | 126.75 kB | 126.71 kB | bundle.min.js

1.25 MB | 670.94 kB | 646.76 kB | 164.72 kB | 163.73 kB | three.js
1.25 MB | 662.90 kB | 646.76 kB | 164.00 kB | 163.73 kB | three.js

2.14 MB | 756.31 kB | 724.14 kB | 182.74 kB | 181.07 kB | victory.js
2.14 MB | 741.41 kB | 724.14 kB | 181.41 kB | 181.07 kB | victory.js

3.20 MB | 1.05 MB | 1.01 MB | 334.08 kB | 331.56 kB | echarts.js
3.20 MB | 1.02 MB | 1.01 MB | 331.95 kB | 331.56 kB | echarts.js

6.69 MB | 2.44 MB | 2.31 MB | 498.86 kB | 488.28 kB | antd.js
6.69 MB | 2.39 MB | 2.31 MB | 496.10 kB | 488.28 kB | antd.js

10.95 MB | 3.59 MB | 3.49 MB | 913.91 kB | 915.50 kB | typescript.js
10.95 MB | 3.56 MB | 3.49 MB | 911.24 kB | 915.50 kB | typescript.js

0 comments on commit 97c8a36

Please sign in to comment.