Skip to content

Commit c0cb248

Browse files
committed
feat(minifier): inline single use variables within the same variable declarations
1 parent 6fcb0d0 commit c0cb248

File tree

5 files changed

+161
-69
lines changed

5 files changed

+161
-69
lines changed

crates/oxc_minifier/src/peephole/minimize_conditions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1123,7 +1123,7 @@ mod test {
11231123

11241124
#[test]
11251125
fn test_coercion_substitution_boolean_result0() {
1126-
test_same("var x = {}, y = x != null;");
1126+
test("var x = {}, y = x != null;", "var y = !0;");
11271127
}
11281128

11291129
#[test]

crates/oxc_minifier/src/peephole/minimize_statements.rs

Lines changed: 113 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,13 @@ impl<'a> PeepholeOptimizations {
394394
ctx.state.changed = true;
395395
}
396396
}
397+
if Self::substitute_single_use_symbol_within_declaration(
398+
var_decl.kind,
399+
&mut var_decl.declarations,
400+
ctx,
401+
) {
402+
ctx.state.changed = true;
403+
}
397404

398405
// If `join_vars` is off, but there are unused declarators ... just join them to make our code simpler.
399406
if !ctx.options().join_vars
@@ -858,6 +865,13 @@ impl<'a> PeepholeOptimizations {
858865
ctx.state.changed = true;
859866
}
860867
}
868+
if Self::substitute_single_use_symbol_within_declaration(
869+
var_decl.kind,
870+
&mut var_decl.declarations,
871+
ctx,
872+
) {
873+
ctx.state.changed = true;
874+
}
861875
}
862876
match_expression!(ForStatementInit) => {
863877
let init = init.to_expression_mut();
@@ -1140,67 +1154,109 @@ impl<'a> PeepholeOptimizations {
11401154
if prev_var_decl.kind.is_using() {
11411155
break;
11421156
}
1157+
let old_len = prev_var_decl.declarations.len();
1158+
let new_len = Self::substitute_single_use_symbol_in_expression_from_declarators(
1159+
expr_in_stmt,
1160+
&mut prev_var_decl.declarations,
1161+
ctx,
1162+
non_scoped_literal_only,
1163+
);
1164+
if new_len == 0 {
1165+
inlined = true;
1166+
stmts.pop();
1167+
} else if old_len != new_len {
1168+
inlined = true;
1169+
prev_var_decl.declarations.truncate(new_len);
1170+
break;
1171+
} else {
1172+
break;
1173+
}
1174+
}
1175+
inlined
1176+
}
11431177

1144-
let last_non_inlined_index =
1145-
prev_var_decl.declarations.iter_mut().rposition(|prev_decl| {
1146-
let Some(prev_decl_init) = &mut prev_decl.init else {
1147-
return true;
1148-
};
1149-
let BindingPatternKind::BindingIdentifier(prev_decl_id) = &prev_decl.id.kind
1150-
else {
1151-
return true;
1152-
};
1153-
if ctx.is_expression_whose_name_needs_to_be_kept(prev_decl_init) {
1154-
return true;
1155-
}
1156-
let Some(symbol_value) =
1157-
ctx.state.symbol_values.get_symbol_value(prev_decl_id.symbol_id())
1158-
else {
1159-
return true;
1160-
};
1161-
// we should check whether it's exported by `symbol_value.exported`
1162-
// because the variable might be exported with `export { foo }` rather than `export var foo`
1163-
if symbol_value.exported
1164-
|| symbol_value.read_references_count > 1
1165-
|| symbol_value.write_references_count > 0
1166-
{
1167-
return true;
1168-
}
1169-
if non_scoped_literal_only && !prev_decl_init.is_literal_value(false, ctx) {
1170-
return true;
1171-
}
1172-
let replaced = Self::substitute_single_use_symbol_in_expression(
1173-
expr_in_stmt,
1174-
&prev_decl_id.name,
1175-
prev_decl_init,
1176-
prev_decl_init.may_have_side_effects(ctx),
1177-
ctx,
1178-
);
1179-
if replaced != Some(true) {
1180-
return true;
1181-
}
1182-
false
1183-
});
1184-
match last_non_inlined_index {
1185-
None => {
1186-
// all inlined
1187-
stmts.pop();
1188-
inlined = true;
1189-
}
1190-
Some(last_non_inlined_index)
1191-
if last_non_inlined_index + 1 == prev_var_decl.declarations.len() =>
1192-
{
1193-
// no change
1194-
break;
1195-
}
1196-
Some(last_non_inlined_index) => {
1197-
prev_var_decl.declarations.truncate(last_non_inlined_index + 1);
1198-
inlined = true;
1199-
break;
1178+
fn substitute_single_use_symbol_within_declaration(
1179+
kind: VariableDeclarationKind,
1180+
declarations: &mut Vec<'a, VariableDeclarator<'a>>,
1181+
ctx: &Ctx<'a, '_>,
1182+
) -> bool {
1183+
// TODO: we should skip this compression when direct eval exists
1184+
// because the code inside eval may reference the variable
1185+
1186+
let mut changed = false;
1187+
if !Self::keep_top_level_var_in_script_mode(ctx) && !kind.is_using() {
1188+
let mut i = 1;
1189+
while i < declarations.len() {
1190+
let (prev_decls, [decl, ..]) = declarations.split_at_mut(i) else { unreachable!() };
1191+
let Some(decl_init) = &mut decl.init else {
1192+
i += 1;
1193+
continue;
1194+
};
1195+
let old_len = prev_decls.len();
1196+
let new_len = Self::substitute_single_use_symbol_in_expression_from_declarators(
1197+
decl_init, prev_decls, ctx, false,
1198+
);
1199+
if old_len != new_len {
1200+
changed = true;
1201+
let drop_count = old_len - new_len;
1202+
declarations.drain(i - drop_count..i);
1203+
i -= drop_count;
12001204
}
1205+
i += 1;
12011206
}
12021207
}
1203-
inlined
1208+
changed
1209+
}
1210+
1211+
/// Returns new length
1212+
fn substitute_single_use_symbol_in_expression_from_declarators(
1213+
target_expr: &mut Expression<'a>,
1214+
declarators: &mut [VariableDeclarator<'a>],
1215+
ctx: &Ctx<'a, '_>,
1216+
non_scoped_literal_only: bool,
1217+
) -> usize {
1218+
let last_non_inlined_index = declarators.iter_mut().rposition(|prev_decl| {
1219+
let Some(prev_decl_init) = &mut prev_decl.init else {
1220+
return true;
1221+
};
1222+
let BindingPatternKind::BindingIdentifier(prev_decl_id) = &prev_decl.id.kind else {
1223+
return true;
1224+
};
1225+
if ctx.is_expression_whose_name_needs_to_be_kept(prev_decl_init) {
1226+
return true;
1227+
}
1228+
let Some(symbol_value) =
1229+
ctx.state.symbol_values.get_symbol_value(prev_decl_id.symbol_id())
1230+
else {
1231+
return true;
1232+
};
1233+
// we should check whether it's exported by `symbol_value.exported`
1234+
// because the variable might be exported with `export { foo }` rather than `export var foo`
1235+
if symbol_value.exported
1236+
|| symbol_value.read_references_count > 1
1237+
|| symbol_value.write_references_count > 0
1238+
{
1239+
return true;
1240+
}
1241+
if non_scoped_literal_only && !prev_decl_init.is_literal_value(false, ctx) {
1242+
return true;
1243+
}
1244+
let replaced = Self::substitute_single_use_symbol_in_expression(
1245+
target_expr,
1246+
&prev_decl_id.name,
1247+
prev_decl_init,
1248+
prev_decl_init.may_have_side_effects(ctx),
1249+
ctx,
1250+
);
1251+
if replaced != Some(true) {
1252+
return true;
1253+
}
1254+
false
1255+
});
1256+
match last_non_inlined_index {
1257+
None => 0,
1258+
Some(last_non_inlined_index) => last_non_inlined_index + 1,
1259+
}
12041260
}
12051261

12061262
/// Returns Some(true) when the expression is successfully replaced.

crates/oxc_minifier/tests/peephole/inline_single_use_variable.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,42 @@ fn test_inline_single_use_variable() {
223223
);
224224
}
225225

226+
#[test]
227+
fn test_within_same_variable_declarations() {
228+
test_script(
229+
"var a = foo, b = a; for (; bar;) console.log(b)",
230+
"for (var a = foo, b = a; bar;) console.log(b)",
231+
);
232+
test(
233+
"function wrapper() { var a = foo, b = a; for (; bar;) return b }",
234+
"function wrapper() { for (var b = foo; bar;) return b }",
235+
);
236+
test(
237+
"function wrapper() { let a = foo, b = a; for (; bar;) return b }",
238+
"function wrapper() { let b = foo; for (; bar;) return b }",
239+
);
240+
test_same("function wrapper() { using a = foo, b = a; for (; bar;) return b }");
241+
test(
242+
"function wrapper() { var a = foo, b = a, c = bar, d = c; for (; baz;) return [b, d] }",
243+
"function wrapper() { for (var b = foo, d = bar; baz;) return [b, d] }",
244+
);
245+
246+
test_script_same("for (var a = foo, b = a; bar;) console.log(b)");
247+
test(
248+
"function wrapper() { for (var a = foo, b = a; bar;) return b }",
249+
"function wrapper() { for (var b = foo; bar;) return b }",
250+
);
251+
test(
252+
"function wrapper() { for (let a = foo, b = a; bar;) return b }",
253+
"function wrapper() { for (let b = foo; bar;) return b }",
254+
);
255+
test_same("function wrapper() { for (using a = foo, b = a; bar;) return b }");
256+
test(
257+
"function wrapper() { for (var a = foo, b = a, c = bar, d = c; baz;) return [b, d] }",
258+
"function wrapper() { for (var b = foo, d = bar; baz;) return [b, d] }",
259+
);
260+
}
261+
226262
#[test]
227263
fn keep_exposed_variables() {
228264
test_same("var x = foo; x(); export { x }");

tasks/minsize/minsize.snap

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,23 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
55

66
173.90 kB | 59.44 kB | 59.82 kB | 19.16 kB | 19.33 kB | 2 | moment.js
77

8-
287.63 kB | 89.28 kB | 90.07 kB | 30.94 kB | 31.95 kB | 2 | jquery.js
8+
287.63 kB | 89.27 kB | 90.07 kB | 30.91 kB | 31.95 kB | 2 | jquery.js
99

1010
342.15 kB | 117 kB | 118.14 kB | 43.19 kB | 44.37 kB | 2 | vue.js
1111

12-
544.10 kB | 71.18 kB | 72.48 kB | 25.85 kB | 26.20 kB | 2 | lodash.js
12+
544.10 kB | 71.15 kB | 72.48 kB | 25.85 kB | 26.20 kB | 2 | lodash.js
1313

1414
555.77 kB | 270.78 kB | 270.13 kB | 88.19 kB | 90.80 kB | 2 | d3.js
1515

1616
1.01 MB | 439.56 kB | 458.89 kB | 122.14 kB | 126.71 kB | 2 | bundle.min.js
1717

18-
1.25 MB | 645.63 kB | 646.76 kB | 159.54 kB | 163.73 kB | 2 | three.js
18+
1.25 MB | 645.62 kB | 646.76 kB | 159.51 kB | 163.73 kB | 2 | three.js
1919

20-
2.14 MB | 713.53 kB | 724.14 kB | 160.99 kB | 181.07 kB | 2 | victory.js
20+
2.14 MB | 713.51 kB | 724.14 kB | 160.96 kB | 181.07 kB | 2 | victory.js
2121

22-
3.20 MB | 1.00 MB | 1.01 MB | 323.10 kB | 331.56 kB | 3 | echarts.js
22+
3.20 MB | 1.00 MB | 1.01 MB | 323.07 kB | 331.56 kB | 3 | echarts.js
2323

24-
6.69 MB | 2.22 MB | 2.31 MB | 459.28 kB | 488.28 kB | 4 | antd.js
24+
6.69 MB | 2.22 MB | 2.31 MB | 458.90 kB | 488.28 kB | 4 | antd.js
2525

26-
10.95 MB | 3.34 MB | 3.49 MB | 855.24 kB | 915.50 kB | 4 | typescript.js
26+
10.95 MB | 3.34 MB | 3.49 MB | 855.22 kB | 915.50 kB | 4 | typescript.js
2727

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
File | File size || Sys allocs | Sys reallocs || Arena allocs | Arena reallocs | Arena bytes
22
-------------------------------------------------------------------------------------------------------------------------------------------
3-
checker.ts | 2.92 MB || 84073 | 14190 || 153691 | 29463 | 5.625 MB
3+
checker.ts | 2.92 MB || 84073 | 14190 || 153733 | 29463 | 5.626 MB
44

5-
cal.com.tsx | 1.06 MB || 40525 | 3033 || 37074 | 4733 | 1.654 MB
5+
cal.com.tsx | 1.06 MB || 40525 | 3033 || 37170 | 4733 | 1.655 MB
66

77
RadixUIAdoptionSection.jsx | 2.52 kB || 82 | 8 || 30 | 6 | 992 B
88

9-
pdf.mjs | 567.30 kB || 19576 | 2900 || 47400 | 7781 | 1.624 MB
9+
pdf.mjs | 567.30 kB || 19576 | 2900 || 47404 | 7781 | 1.624 MB
1010

11-
antd.js | 6.69 MB || 99854 | 13518 || 331725 | 70117 | 17.407 MB
11+
antd.js | 6.69 MB || 99739 | 13518 || 331915 | 69883 | 17.335 MB
1212

1313
binder.ts | 193.08 kB || 4768 | 974 || 7059 | 834 | 201.192 kB
1414

0 commit comments

Comments
 (0)