From 3dc2d8b8e94e8374e7efb1a6aeae12cf87566ee9 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 17 Jan 2025 06:00:06 +0000 Subject: [PATCH] feat(minifier): fold string concat chaining (#8441) Compress `"".concat(a).concat(b)` into `"".concat(a, b)`. **References** - [Spec of `String::concat`](https://tc39.es/ecma262/multipage/text-processing.html#sec-string.prototype.concat) --- .../peephole_replace_known_methods.rs | 27 +++++++++++++++---- tasks/minsize/minsize.snap | 6 ++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs b/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs index c8349273c22a8..0b9bc0e72b941 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs @@ -26,7 +26,7 @@ impl<'a> CompressorPass<'a> for PeepholeReplaceKnownMethods { impl<'a> Traverse<'a> for PeepholeReplaceKnownMethods { fn exit_expression(&mut self, node: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { - self.try_fold_array_concat(node, ctx); + self.try_fold_concat_chain(node, ctx); self.try_fold_known_string_methods(node, ctx); } } @@ -341,7 +341,8 @@ impl<'a> PeepholeReplaceKnownMethods { } /// `[].concat(a).concat(b)` -> `[].concat(a, b)` - fn try_fold_array_concat(&mut self, node: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + /// `"".concat(a).concat(b)` -> `"".concat(a, b)` + fn try_fold_concat_chain(&mut self, node: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { if matches!(ctx.parent(), Ancestor::StaticMemberExpressionObject(_)) { return; } @@ -368,7 +369,7 @@ impl<'a> PeepholeReplaceKnownMethods { // We don't need to check if the arguments has a side effect here. // - // The only side effect Array::concat can cause is throwing an error when the created array is too long. + // The only side effect Array::concat / String::concat can cause is throwing an error when the created array is too long. // With the compressor assumption, that error can be moved. // // For example, if we have `[].concat(a).concat(b)`, the steps before the compression is: @@ -387,10 +388,13 @@ impl<'a> PeepholeReplaceKnownMethods { let CallExpression { callee, arguments, .. } = ce.as_mut(); collected_arguments.push(arguments); - // [].concat() + // [].concat() or "".concat() let is_root_expr_concat = { let Expression::StaticMemberExpression(member) = callee else { unreachable!() }; - matches!(&member.object, Expression::ArrayExpression(_)) + matches!( + &member.object, + Expression::ArrayExpression(_) | Expression::StringLiteral(_) + ) }; if is_root_expr_concat { new_root_callee = callee; @@ -1162,6 +1166,7 @@ mod test { #[test] fn test_fold_concat_chaining() { + // array fold("[1,2].concat(1).concat(2,['abc']).concat('abc')", "[1,2].concat(1,2,['abc'],'abc')"); fold("[].concat(['abc']).concat(1).concat([2,3])", "[].concat(['abc'],1,[2,3])"); @@ -1170,6 +1175,18 @@ mod test { fold("var x; [1].concat(x.a).concat(x)", "var x; [1].concat(x.a, x)"); // x.a might have a getter that updates x, but that side effect is preserved correctly fold_same("[].concat(1)"); + + // string + fold("'1'.concat(1).concat(2,['abc']).concat('abc')", "'1'.concat(1,2,['abc'],'abc')"); + fold("''.concat(['abc']).concat(1).concat([2,3])", "''.concat(['abc'],1,[2,3])"); + + fold("var x, y; ''.concat(x).concat(y)", "var x, y; ''.concat(x, y)"); + fold("var y; ''.concat(x).concat(y)", "var y; ''.concat(x, y)"); // x might have a getter that updates y, but that side effect is preserved correctly + fold("var x; ''.concat(x.a).concat(x)", "var x; ''.concat(x.a, x)"); // x.a might have a getter that updates x, but that side effect is preserved correctly + + fold_same("''.concat(1)"); + + // other fold_same("obj.concat([1,2]).concat(1)"); } diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 0e8af93b80a83..d3e21c4fdcb8f 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -17,11 +17,11 @@ Original | minified | minified | gzip | gzip | Fixture 1.25 MB | 652.88 kB | 646.76 kB | 163.54 kB | 163.73 kB | three.js -2.14 MB | 725.56 kB | 724.14 kB | 180.06 kB | 181.07 kB | victory.js +2.14 MB | 724.06 kB | 724.14 kB | 179.94 kB | 181.07 kB | victory.js 3.20 MB | 1.01 MB | 1.01 MB | 332.01 kB | 331.56 kB | echarts.js -6.69 MB | 2.32 MB | 2.31 MB | 492.65 kB | 488.28 kB | antd.js +6.69 MB | 2.32 MB | 2.31 MB | 492.44 kB | 488.28 kB | antd.js -10.95 MB | 3.49 MB | 3.49 MB | 907.34 kB | 915.50 kB | typescript.js +10.95 MB | 3.49 MB | 3.49 MB | 907.09 kB | 915.50 kB | typescript.js