mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(minifier): minimize array join expression (#27905)
This commit is contained in:
1 parent
88b5c3580b
commit
7bb2697580
4 files changed
+137
-115
No files matched your search
@@ -6,7 +6,7 @@ use oxc_allocator::{ArenaBox, ArenaVec, GetAllocator, TakeIn};
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_compat::ESFeature;
|
||||
use oxc_ecmascript::{
|
||||
StringCharAt, StringCharAtResult, ToBigInt, ToIntegerIndex,
|
||||
ArrayJoin, StringCharAt, StringCharAtResult, ToBigInt, ToIntegerIndex, ToJsString,
|
||||
constant_evaluation::{ConstantEvaluation, DetermineValueType},
|
||||
side_effects::{MayHaveSideEffects, is_regexp_syntax_supported},
|
||||
};
|
||||
@@ -50,6 +50,7 @@ impl<'a> PeepholeOptimizations {
|
||||
};
|
||||
let replacement = match name {
|
||||
"concat" => Self::try_fold_concat(*span, arguments, callee, ctx),
|
||||
"join" => Self::try_fold_array_join(*span, arguments, object, ctx),
|
||||
"pow" => Self::try_fold_pow(*span, arguments, object, ctx),
|
||||
"of" => Self::try_fold_array_of(*span, arguments, name, object, ctx),
|
||||
_ => None,
|
||||
@@ -362,6 +363,46 @@ impl<'a> PeepholeOptimizations {
|
||||
}
|
||||
}
|
||||
|
||||
/// `['a','b'].join()` -> `'a,b'`
|
||||
fn try_fold_array_join(
|
||||
span: Span,
|
||||
arguments: &Arguments<'a>,
|
||||
object: &Expression<'a>,
|
||||
ctx: &TraverseCtx<'a>,
|
||||
) -> Option<Expression<'a>> {
|
||||
let Expression::ArrayExpression(array) = object else { return None };
|
||||
if array.may_have_side_effects(ctx) || arguments.len() > 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let separator = if let Some(argument) = arguments.first() {
|
||||
let separator = argument.as_expression()?;
|
||||
if separator.may_have_side_effects(ctx) {
|
||||
return None;
|
||||
}
|
||||
if separator.value_type(ctx).is_undefined() {
|
||||
None
|
||||
} else {
|
||||
Some(separator.to_js_string(ctx)?)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let separator = separator.as_deref().unwrap_or(",");
|
||||
|
||||
let per_element = array.elements.len().saturating_sub(1) * 3;
|
||||
let syntax_savings = per_element + "[].join()".len();
|
||||
|
||||
let added_size = separator.len() * (array.elements.len().saturating_sub(2));
|
||||
|
||||
if syntax_savings <= added_size {
|
||||
return None;
|
||||
}
|
||||
|
||||
let joined = array.array_join(ctx, Some(separator))?;
|
||||
Some(Expression::new_string_literal(span, Str::from_str_in(&joined, ctx), None, ctx))
|
||||
}
|
||||
|
||||
pub fn escape_string_for_template_literal(s: &str) -> Cow<'_, str> {
|
||||
if s.contains(['\\', '`', '$', '\r']) {
|
||||
Cow::Owned(
|
||||
|
||||
@@ -56,85 +56,110 @@ fn test_string_index_of() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "TODO: Array.join optimization with sparse arrays not yet implemented"]
|
||||
fn test_string_join_add_sparse() {
|
||||
test("x = [,,'a'].join(',')", "x = ',,a'");
|
||||
}
|
||||
fn test_try_fold_array_join() {
|
||||
test_value("[].join()", "''");
|
||||
test_value("[].join(',')", "''");
|
||||
test_value("['a'].join('')", "'a'");
|
||||
test_value("['a'].join(',')", "'a'");
|
||||
test_value("['a', 'b', 'c'].join()", "'a,b,c'");
|
||||
test_value("['a', 'b', 'c'].join('')", "'abc'");
|
||||
test_value("['a', 'b', 'c'].join(',')", "'a,b,c'");
|
||||
test_value("['a', 'b'].join(undefined)", "'a,b'");
|
||||
test_value("['a', 'b'].join(null)", "'anullb'");
|
||||
|
||||
#[test]
|
||||
#[ignore = "TODO: Array.join optimization edge cases not yet implemented"]
|
||||
fn test_no_string_join() {
|
||||
test_same("x = [].join(',',2)");
|
||||
test_same("x = [].join(f)");
|
||||
// Works with numbers
|
||||
test_value("['a=', 5].join('')", "'a=5'");
|
||||
test_value("['a', '5'].join(7)", "'a75'");
|
||||
test_value("[1, 2, 3].join('-')", "'1-2-3'");
|
||||
test_value("[1,2,3].join('abcdef')", "'1abcdef2abcdef3'");
|
||||
test_value("[1,2].join()", "'1,2'");
|
||||
test_value("[[1, 2], [3, 4]].join()", "'1,2,3,4'");
|
||||
|
||||
// Works on boolean
|
||||
test_value("['a=', false].join('')", "'a=false'");
|
||||
test_value("['a', '5'].join(true)", "'atrue5'");
|
||||
test_value("['a', '5'].join(false)", "'afalse5'");
|
||||
|
||||
// Template strings
|
||||
test_value("[`a`, `b`, `c`].join(``)", "'abc'");
|
||||
test_value("[`a`, `b`, `c`].join('')", "'abc'");
|
||||
|
||||
// Works on sparse
|
||||
test_value("[,,'a'].join(',')", "',,a'");
|
||||
|
||||
// Works on nullish
|
||||
test_value("[null].join('')", "''");
|
||||
test_value("[null,undefined,''].join(',')", "',,'");
|
||||
test_value("[null,undefined,0].join(',')", "',,0'");
|
||||
|
||||
// Only optimize if it's a size win.
|
||||
test_value("['a'].join('a very very very long chain')", "'a'");
|
||||
test_value(
|
||||
"['a', '5', 'c'].join('a very very very long chain')",
|
||||
"['a','5','c'].join('a very very very long chain')",
|
||||
);
|
||||
|
||||
// Unsupported syntax or side effects
|
||||
test_same_value("[].join(',',2)");
|
||||
test_same_value("[].join(f)");
|
||||
test_same_value("[foo()].join(',')");
|
||||
test_same_value("[void foo()].join(',')");
|
||||
test_same_value("[1].join(void foo())");
|
||||
test_same_value("[1].join(f)");
|
||||
test_same_value("[foo()].join('')");
|
||||
test_same("[foo()].join('')");
|
||||
test_same_value("[x].join();");
|
||||
test_same_value("[x,y].join();");
|
||||
test_same_value("[x,y,z].join();");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "TODO: Array.join to string concatenation optimization not yet implemented"]
|
||||
fn test_string_join_add() {
|
||||
test("x = ['a', 'b', 'c'].join('')", "x = \"abc\"");
|
||||
test("x = [].join(',')", "x = \"\"");
|
||||
test("x = ['a'].join(',')", "x = \"a\"");
|
||||
test("x = ['a', 'b', 'c'].join(',')", "x = \"a,b,c\"");
|
||||
test("x = ['a', foo, 'b', 'c'].join(',')", "x = [\"a\",foo,\"b,c\"].join()");
|
||||
test("x = [foo, 'a', 'b', 'c'].join(',')", "x = [foo,\"a,b,c\"].join()");
|
||||
test("x = ['a', 'b', 'c', foo].join(',')", "x = [\"a,b,c\",foo].join()");
|
||||
|
||||
// Works with numbers
|
||||
test("x = ['a=', 5].join('')", "x = \"a=5\"");
|
||||
test("x = ['a', '5'].join(7)", "x = \"a75\"");
|
||||
|
||||
// Works on boolean
|
||||
test("x = ['a=', false].join('')", "x = \"a=false\"");
|
||||
test("x = ['a', '5'].join(true)", "x = \"atrue5\"");
|
||||
test("x = ['a', '5'].join(false)", "x = \"afalse5\"");
|
||||
|
||||
// Only optimize if it's a size win.
|
||||
test(
|
||||
"x = ['a', '5', 'c'].join('a very very very long chain')",
|
||||
"x = [\"a\",\"5\",\"c\"].join(\"a very very very long chain\")",
|
||||
);
|
||||
|
||||
// Template strings
|
||||
test("x = [`a`, `b`, `c`].join(``)", "x = 'abc'");
|
||||
test("x = [`a`, `b`, `c`].join('')", "x = 'abc'");
|
||||
test_value("['a', foo, 'b', 'c'].join(',')", "['a',foo,'b,c'].join()");
|
||||
test_value("[foo, 'a', 'b', 'c'].join(',')", "[foo,'a,b,c'].join()");
|
||||
test_value("['a', 'b', 'c', foo].join(',')", "['a,b,c',foo].join()");
|
||||
|
||||
// TODO(user): Its possible to fold this better.
|
||||
test_same("x = ['', foo].join('-')");
|
||||
test_same("x = ['', foo, ''].join()");
|
||||
test_same_value("['', foo].join('-')");
|
||||
test_same_value("['', foo, ''].join()");
|
||||
|
||||
test(
|
||||
"x = ['', '', foo, ''].join(',')", //
|
||||
"x = [ ',' , foo, ''].join()",
|
||||
test_value(
|
||||
"['', '', foo, ''].join(',')", //
|
||||
"[ ',' , foo, ''].join()",
|
||||
);
|
||||
test(
|
||||
"x = ['', '', foo, '', ''].join(',')", //
|
||||
"x = [ ',', foo, ','].join()",
|
||||
test_value(
|
||||
"['', '', foo, '', ''].join(',')", //
|
||||
"[ ',', foo, ','].join()",
|
||||
);
|
||||
|
||||
test(
|
||||
"x = ['', '', foo, '', '', bar].join(',')", //
|
||||
"x = [ ',', foo, ',', bar].join()",
|
||||
test_value(
|
||||
"['', '', foo, '', '', bar].join(',')", //
|
||||
"[ ',', foo, ',', bar].join()",
|
||||
);
|
||||
|
||||
test(
|
||||
"x = [1,2,3].join('abcdef')", //
|
||||
"x = '1abcdef2abcdef3'",
|
||||
);
|
||||
|
||||
test("x = [1,2].join()", "x = '1,2'");
|
||||
test("x = [null,undefined,''].join(',')", "x = ',,'");
|
||||
test("x = [null,undefined,0].join(',')", "x = ',,0'");
|
||||
// This can be folded but we don't currently.
|
||||
test_same("x = [[1,2],[3,4]].join()"); // would like: "x = '1,2,3,4'"
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "TODO: Array.join single element optimization not yet implemented"]
|
||||
fn test_string_join_add_b1992789() {
|
||||
test("x = ['a'].join('')", "x = \"a\"");
|
||||
test_same("x = [foo()].join('')");
|
||||
test_same("[foo()].join('')");
|
||||
test("[null].join('')", "''");
|
||||
fn test_join_spread1() {
|
||||
test_same_value("[...foo].join('')");
|
||||
test_same_value("[...someMap.keys()].join('')");
|
||||
test_same_value("[foo, ...bar].join('')");
|
||||
test_same_value("[...foo, bar].join('')");
|
||||
test_same_value("[...foo, 'bar'].join('')");
|
||||
test_same_value("['1', ...'2', '3'].join('')");
|
||||
test_value("['1', ...['2'], '3'].join('')", "'123'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "TODO: Array.join with spread syntax optimization not yet implemented"]
|
||||
fn test_join_spread2() {
|
||||
test_value("[...foo].join(',')", "[...foo].join()");
|
||||
test_value("[...someMap.keys()].join(',')", "[...someMap.keys()].join()");
|
||||
test_value("[foo, ...bar].join(',')", "[foo, ...bar].join()");
|
||||
test_value("[...foo, bar].join(',')", "[...foo, bar].join()");
|
||||
test_value("[...foo, 'bar'].join(',')", "[...foo, 'bar'].join()");
|
||||
test_value("['1', ...'2', '3'].join(',')", "['1', ...'2', '3'].join()");
|
||||
test_value("['1', ...['2'], '3'].join(',')", "['1', ...['2'], '3'].join()");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -320,50 +345,6 @@ fn test_fold_string_split() {
|
||||
// test_same("x = 'a b c d e'.split(' ')");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "TODO: Array.join edge case optimization not yet implemented"]
|
||||
fn test_join_bug() {
|
||||
test("var x = [].join();", "var x = '';");
|
||||
test_same("var x = [x].join();");
|
||||
test_same("var x = [x,y].join();");
|
||||
test_same("var x = [x,y,z].join();");
|
||||
|
||||
// test_same(
|
||||
// lines(
|
||||
// "shape['matrix'] = [",
|
||||
// " Number(headingCos2).toFixed(4),",
|
||||
// " Number(-headingSin2).toFixed(4),",
|
||||
// " Number(headingSin2 * yScale).toFixed(4),",
|
||||
// " Number(headingCos2 * yScale).toFixed(4),",
|
||||
// " 0,",
|
||||
// " 0",
|
||||
// " ].join()"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "TODO: Array.join with spread syntax optimization not yet implemented"]
|
||||
fn test_join_spread1() {
|
||||
test_same("var x = [...foo].join('');");
|
||||
test_same("var x = [...someMap.keys()].join('');");
|
||||
test_same("var x = [foo, ...bar].join('');");
|
||||
test_same("var x = [...foo, bar].join('');");
|
||||
test_same("var x = [...foo, 'bar'].join('');");
|
||||
test_same("var x = ['1', ...'2', '3'].join('');");
|
||||
test_same("var x = ['1', ...['2'], '3'].join('');");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "TODO: Array.join with spread syntax optimization not yet implemented"]
|
||||
fn test_join_spread2() {
|
||||
test("var x = [...foo].join(',');", "var x = [...foo].join();");
|
||||
test("var x = [...someMap.keys()].join(',');", "var x = [...someMap.keys()].join();");
|
||||
test("var x = [foo, ...bar].join(',');", "var x = [foo, ...bar].join();");
|
||||
test("var x = [...foo, bar].join(',');", "var x = [...foo, bar].join();");
|
||||
test("var x = [...foo, 'bar'].join(',');", "var x = [...foo, 'bar'].join();");
|
||||
test("var x = ['1', ...'2', '3'].join(',');", "var x = ['1', ...'2', '3'].join();");
|
||||
test("var x = ['1', ...['2'], '3'].join(',');", "var x = ['1', ...['2'], '3'].join();");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_upper() {
|
||||
test("x = 'a'.toUpperCase()", "x = 'A'");
|
||||
@@ -913,7 +894,7 @@ fn test_fold_concat_chaining() {
|
||||
// array
|
||||
test("x = [1,2].concat(1).concat(2,['abc']).concat('abc')", "x = [1,2,1,2,'abc','abc']");
|
||||
test("x = [].concat(['abc']).concat(1).concat([2,3])", "x = ['abc',1,2,3]");
|
||||
test("x = [].concat(1).concat(2).join(',')", "x = [1,2].join(',')");
|
||||
test("x = [].concat(1).concat(2).join(',')", "x = '1,2'");
|
||||
|
||||
test("var x, y; [1].concat(x).concat(y)", "var x, y; [1].concat(x, y)");
|
||||
test("var y; [1].concat(x).concat(y)", "var y; [1].concat(x, y)"); // x might have a getter that updates y, but that side effect is preserved correctly
|
||||
|
||||
@@ -15,11 +15,11 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
|
||||
|
||||
1.01 MB | 435.88 kB | 458.89 kB | 121.78 kB | 126.71 kB | 2 | bundle.min.js
|
||||
|
||||
1.25 MB | 634.22 kB | 646.76 kB | 158.42 kB | 163.73 kB | 2 | three.js
|
||||
1.25 MB | 634.21 kB | 646.76 kB | 158.43 kB | 163.73 kB | 2 | three.js
|
||||
|
||||
2.14 MB | 707.58 kB | 724.14 kB | 160.05 kB | 181.07 kB | 2 | victory.js
|
||||
|
||||
3.20 MB | 958.40 kB | 1.01 MB | 316.96 kB | 331.56 kB | 2 | echarts.js
|
||||
3.20 MB | 958.38 kB | 1.01 MB | 316.95 kB | 331.56 kB | 2 | echarts.js
|
||||
|
||||
6.69 MB | 2.12 MB | 2.31 MB | 453.41 kB | 488.28 kB | 4 | antd.js
|
||||
|
||||
|
||||
@@ -66,11 +66,11 @@ binder.ts:
|
||||
|
||||
kitchen-sink.tsx:
|
||||
file size: 732898 # 732.90 kB
|
||||
sys allocs: 1854
|
||||
sys allocs: 1857
|
||||
sys reallocs: 174
|
||||
sys deallocs: 1854
|
||||
sys deallocs: 1857
|
||||
sys alloc bytes: 5304252 # 5.30 MB
|
||||
sys peak growth: 3707425 # 3.71 MB
|
||||
arena allocs: 31112
|
||||
arena reallocs: 3012
|
||||
arena allocs: 31114
|
||||
arena reallocs: 3011
|
||||
arena size: 8461040 # 8.46 MB
|
||||
Reference in new issue
Block a user