Skip to content

Commit

Permalink
feat(html/minifier): Merge identical metadata elements (#6183)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed Oct 18, 2022
1 parent 789ea16 commit 41093b0
Show file tree
Hide file tree
Showing 17 changed files with 227 additions and 9 deletions.
51 changes: 47 additions & 4 deletions crates/swc_html_minifier/src/lib.rs
Expand Up @@ -8,8 +8,8 @@ use serde_json::Value;
use swc_atoms::{js_word, JsWord};
use swc_cached::regex::CachedRegex;
use swc_common::{
collections::AHashMap, comments::SingleThreadedComments, sync::Lrc, FileName, FilePathMapping,
Mark, SourceMap, DUMMY_SP,
collections::AHashMap, comments::SingleThreadedComments, sync::Lrc, EqIgnoreSpan, FileName,
FilePathMapping, Mark, SourceMap, DUMMY_SP,
};
use swc_html_ast::*;
use swc_html_parser::parser::ParserConfig;
Expand Down Expand Up @@ -1201,6 +1201,39 @@ impl Minifier<'_> {
true
}

fn allow_elements_to_merge(&self, left: Option<&Child>, right: &Element) -> bool {
if let Some(left) = left {
return matches!((left, right), (Child::Element(left), right)
if matches!(left.namespace, Namespace::HTML | Namespace::SVG)
&& left.tag_name == js_word!("style")
&& matches!(right.namespace, Namespace::HTML | Namespace::SVG)
&& right.tag_name == js_word!("style")
&& left.attributes.eq_ignore_span(&right.attributes));
}

false
}

fn merge_text_children(&self, left: &Element, right: &Element) -> Vec<Child> {
let data = left.children.iter().chain(right.children.iter()).fold(
String::new(),
|mut acc, child| match child {
Child::Text(text) => {
acc.push_str(&text.data);

acc
}
_ => acc,
},
);

vec![Child::Text(Text {
span: DUMMY_SP,
data: data.into(),
raw: None,
})]
}

fn minify_children(&mut self, children: &mut Vec<Child>) -> Vec<Child> {
let (namespace, tag_name) = match &self.current_element {
Some(element) => (element.namespace, &element.tag_name),
Expand All @@ -1212,7 +1245,7 @@ impl Minifier<'_> {
let mode = self.get_whitespace_minification_for_tag(namespace, tag_name);

let child_will_be_retained =
|child: &mut Child, prev_children: &Vec<Child>, next_children: &Vec<Child>| {
|child: &mut Child, prev_children: &mut Vec<Child>, next_children: &mut Vec<Child>| {
match child {
Child::Comment(comment) if self.options.remove_comments => {
self.is_preserved_comment(&comment.data)
Expand All @@ -1233,6 +1266,16 @@ impl Minifier<'_> {
{
false
}
Child::Element(element)
if self.options.merge_metadata_elements
&& self.allow_elements_to_merge(prev_children.last(), element) =>
{
if let Some(Child::Element(prev)) = prev_children.last_mut() {
prev.children = self.merge_text_children(prev, element);
}

false
}
Child::Text(text) if text.data.is_empty() => false,
Child::Text(text)
if self.need_collapse_whitespace()
Expand Down Expand Up @@ -1539,7 +1582,7 @@ impl Minifier<'_> {
}
};

let result = child_will_be_retained(&mut child, &new_children, children);
let result = child_will_be_retained(&mut child, &mut new_children, children);

if result {
new_children.push(child);
Expand Down
4 changes: 4 additions & 0 deletions crates/swc_html_minifier/src/option.rs
Expand Up @@ -140,6 +140,10 @@ pub struct MinifyOptions {
pub remove_redundant_attributes: bool,
#[serde(default = "true_by_default")]
pub collapse_boolean_attributes: bool,
/// Merge the same metadata elements into one (for example, consecutive
/// `style` elements will be merged into one `style` element)
#[serde(default = "true_by_default")]
pub merge_metadata_elements: bool,
/// Remove extra whitespace in space and comma separated attribute values
/// (where it is safe) and remove `javascript:` prefix for event handler
/// attributes
Expand Down
Expand Up @@ -10,6 +10,8 @@
<button type="button" onclick="a(1 + 2)" ng-click="a(1 + 2)" data-click="a(1 + 2)"></button>
<button type="button" onclick="a(1 + 2)" ng-click="a(1 + 2)" data-click="a(1 + 2)"></button>
<div data-json='{ "foo": "bar" }'></div>
<div data-json="{ &quot;foo&quot;: &quot;bar&quot; }"></div>
<div data-json='{ &quot;foo&quot;: &quot;bar&quot; }'></div>
<div data-style="color: red; background-color: red"></div>
<iframe srcdoc="<html> <body> <p>test.</p>" src="nosrcdoc.html"></iframe>
<div data-html="<html> <body> <p>test.</p>" src="nosrcdoc.html"></div>
Expand Down
@@ -1,6 +1,8 @@
<!doctype html><html lang=en><meta charset=UTF-8><meta name=viewport content="width=device-width,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0"><title>Document</title><button type=button onclick=a(3) ng-click=a(3) data-click=a(3)></button>
<button type=button onclick=a(3) ng-click=a(3) data-click=a(3)></button>
<div data-json='{"foo":"bar"}'></div>
<div data-json='{"foo":"bar"}'></div>
<div data-json='{"foo":"bar"}'></div>
<div data-style=color:red;background-color:red></div>
<iframe srcdoc="<p>test." src=nosrcdoc.html></iframe>
<div data-html="<p>test." src=nosrcdoc.html></div>
@@ -0,0 +1,9 @@
<!doctype html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<iframe srcdoc="<p>hello<!-- comment --> </p>"></iframe>
</body>
</html>
@@ -0,0 +1 @@
<!doctype html><html lang=en><title>Document</title><iframe srcdoc="<p>hello"></iframe>
Expand Up @@ -10,5 +10,6 @@
<CUSTOM-TAG></CUSTOM-TAG><div>Hello :)</div>
<tag v-ref:vm_pv :imgs=" objpicsurl_ "></tag>
<span><phrasing-element></phrasing-element></span>
<p>Click <my-link>here</my-link> and <my-link>here</my-link></p>
</body>
</html>
Expand Up @@ -3,4 +3,5 @@
[\']["]
<custom-tag></custom-tag><div>Hello :)</div>
<tag v-ref:vm_pv :imgs=" objpicsurl_ "></tag>
<span><phrasing-element></phrasing-element></span>
<span><phrasing-element></phrasing-element></span>
<p>Click <my-link>here</my-link> and <my-link>here</my-link>
@@ -0,0 +1,3 @@
{
"removeEmptyMetadataElements": false
}
@@ -0,0 +1,64 @@
<!doctype html>
<html lang="en">
<head>
<title>Document</title>
<style>
a {
color:red
}
</style><style>
b {
color:blue
}
</style>
<!-- test -->
<style>
p {
color: white;
background-color: blue;
padding: 5px;
border: 1px solid black;
}
</style>
<style>
p {
color: blue;
background-color: yellow;
}
</style>
<style media="all and (max-width: 500px)">
p {
color: blue;
background-color: yellow;
}
</style>
<style type="text/css">
.first {
color: red;
}
</style>
<style type="text/css">
.second {
color: red;
}
</style>
<style media="all">
p {
color: blue;
}
</style>
<style media="all">
p {
color: red;
}
</style>
</head>
<body>
<div>test</div>
<style>a { color: red }</style>
<style></style>
<div>test</div>
<style></style>
<style>a { color: red }</style>
</body>
</html>
@@ -0,0 +1,4 @@
<!doctype html><html lang=en><title>Document</title><style>a{color:red}b{color:blue}p{color:white;background-color:blue;padding:5px;border:1px solid black}p{color:blue;background-color:yellow}</style><style media="all and (max-width:500px)">p{color:blue;background-color:yellow}</style><style>.first{color:red}.second{color:red}</style><style media=all>p{color:blue}p{color:red}</style><div>test</div>
<style>a{color:red}</style>
<div>test</div>
<style>a{color:red}</style>
@@ -0,0 +1,76 @@
<!doctype html>
<html lang="en">
<head>
<title>Document</title>
<style>
a {
color:red
}
</style><style>
b {
color:blue
}
</style>
<!-- test -->
<style>
p {
color: white;
background-color: blue;
padding: 5px;
border: 1px solid black;
}
</style>
<style>
p {
color: blue;
background-color: yellow;
}
</style>
<style media="all and (max-width: 500px)">
p {
color: blue;
background-color: yellow;
}
</style>
<style type="text/css">
.first {
color: red;
}
</style>
<style type="text/css">
.second {
color: red;
}
</style>
<style media="all">
p {
color: blue;
}
</style>
<style media="all">
p {
color: red;
}
</style>
</head>
<body>
<h1>Text</h1>
<div>Text</div>
<style>
a {
color:red
}
</style>
<style>
b {
color:blue
}
</style>
<div>test</div>
<style>a { color: red }</style>
<style></style>
<div>test</div>
<style></style>
<style>a { color: red }</style>
</body>
</html>
@@ -0,0 +1,8 @@
<!doctype html><html lang=en><title>Document</title><style>a{color:red}b{color:blue}p{color:white;background-color:blue;padding:5px;border:1px solid black}p{color:blue;background-color:yellow}</style><style media="all and (max-width:500px)">p{color:blue;background-color:yellow}</style><style>.first{color:red}.second{color:red}</style><style media=all>p{color:blue}p{color:red}</style><h1>Text</h1>
<div>Text</div>
<style>a{color:red}b{color:blue}</style>
<div>test</div>
<style>a{color:red}</style>
<div>test</div>

<style>a{color:red}</style>
Expand Up @@ -4,7 +4,7 @@
<svg>
<style>a{color:red}</style>
</svg>
<style>p{color:red}</style><style>a{color:red}@media all{p{color:red}}</style><style>.test{color:red}</style>
<style>p{color:red}a{color:red}@media all{p{color:red}}.test{color:red}</style>
<svg viewBox="0 0 10 10">
<style>circle{fill:gold;stroke:maroon;stroke-width:2px}</style>

Expand Down
Expand Up @@ -29,4 +29,4 @@
foo

baz
</pre> <div> a <input> c </div> <div>Empty </div> <!--[if lte IE 6]> <span>A</span> <span title=" sigificant whitespace ">blah blah</span> <![endif]--> <div> <a href=#> <span> <b> foo </b> <i> bar </i> </span> </a> </div> <div>a b</div> <div>a b c d</div> <div> text </div> <span> text </span> <span> text </span> <div> <style>a{color:red}</style> <span>test</span> <style>a{color:red}</style> </div> <div> <style>a{color:red}</style><style>a{color:red}</style> </div> <div> <style>a{color:red}</style> <span>test</span> <span>test</span> <style>a{color:red}</style> </div> <div> <script>console.log("test")</script><script>console.log("test")</script> </div>
</pre> <div> a <input> c </div> <div>Empty </div> <!--[if lte IE 6]> <span>A</span> <span title=" sigificant whitespace ">blah blah</span> <![endif]--> <div> <a href=#> <span> <b> foo </b> <i> bar </i> </span> </a> </div> <div>a b</div> <div>a b c d</div> <div> text </div> <span> text </span> <span> text </span> <div> <style>a{color:red}</style> <span>test</span> <style>a{color:red}</style> </div> <div> <style>a{color:red}a{color:red}</style> </div> <div> <style>a{color:red}</style> <span>test</span> <span>test</span> <style>a{color:red}</style> </div> <div> <script>console.log("test")</script><script>console.log("test")</script> </div>
Expand Up @@ -275,7 +275,7 @@
</div>

<div>
<style>a{color:red}</style><style>a{color:red}</style>
<style>a{color:red}a{color:red}</style>
</div>

<div>
Expand Down
Expand Up @@ -29,4 +29,4 @@
foo

baz
</pre><div>a <input> c</div><div>Empty</div><!--[if lte IE 6]> <span>A</span> <span title=" sigificant whitespace ">blah blah</span> <![endif]--><div><a href=#> <span><b>foo </b><i> bar </i></span></a></div><div>a b</div><div>a b c d</div><div>text</div><span> text </span><span> text </span><div><span>test</span> <span>test</span></div><div><span>test</span> <command>test</command><span>test</span></div><div><span>test</span><link rel=stylesheet href=""> <span>test</span></div><div><span>test</span><meta name=content> <span>test</span></div><div><span>test</span><script>console.log("test")</script> <span>test</span></div><div><span>test</span><style>a{color:red}</style> <span>test</span></div><div><span>test</span><title>test</title> <span>test</span></div><div><meta name=test><meta name=test></div><div><link rel=stylesheet href=""><link rel=stylesheet href=""></div><div><script>console.log("test")</script><script>console.log("test")</script></div><div><script>console.log("test")</script> <span>test</span><script>console.log("test")</script></div><div><style>a{color:red}</style><style>a{color:red}</style></div><div><script>console.log("test")</script><style>a{color:red}</style></div><div><span itemscope><meta itemprop=name content="The Castle">test</span> <span>test</span></div><div><meta name=test></div><div><style>a{color:red}</style></div><div><meta name=test><div>test</div><meta name=test></div><div><meta name=test> <span>test</span><meta name=test></div><svg> <title>test</title> <metadata>test</metadata> <desc>test</desc> </svg><svg> <a>test</a> <a>test</a> </svg><svg><text x=20 y=35><tspan font-weight=bold fill=red>This is bold and red</tspan> <tspan font-weight=bold fill=red>This is bold and red</tspan></text></svg><svg> <tspan>test</tspan><foreignObject>test</foreignObject></svg><svg><text x=20 y=35><tspan font-weight=bold fill=red>This is bold and red</tspan> <tspan font-weight=bold fill=red>This is bold and red</tspan></text></svg><svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice" style=width:100%;height:100%;position:absolute;top:0;left:0;z-index:-1> <linearGradient id=gradient><stop class=begin offset=0% /><stop class=end offset=100% /></linearGradient><rect width=100 height=100 style=fill:url(#gradient) /> <circle cx=50 cy=50 r=30 style=fill:url(#gradient) /> </svg><svg> <script>console.log("test")</script></svg><svg> <style>a{color:red}</style></svg><div><span>test</span> a b <span>test</span> <span>test</span> a b <span>test</span> <span>test</span> a b <span>test</span></div><div><foo-bar> <span>test</span> </foo-bar> <foo-bar> <span>test</span> </foo-bar></div><div><svg> <linearGradient id=gradient /> </svg><span>a</span></div>
</pre><div>a <input> c</div><div>Empty</div><!--[if lte IE 6]> <span>A</span> <span title=" sigificant whitespace ">blah blah</span> <![endif]--><div><a href=#> <span><b>foo </b><i> bar </i></span></a></div><div>a b</div><div>a b c d</div><div>text</div><span> text </span><span> text </span><div><span>test</span> <span>test</span></div><div><span>test</span> <command>test</command><span>test</span></div><div><span>test</span><link rel=stylesheet href=""> <span>test</span></div><div><span>test</span><meta name=content> <span>test</span></div><div><span>test</span><script>console.log("test")</script> <span>test</span></div><div><span>test</span><style>a{color:red}</style> <span>test</span></div><div><span>test</span><title>test</title> <span>test</span></div><div><meta name=test><meta name=test></div><div><link rel=stylesheet href=""><link rel=stylesheet href=""></div><div><script>console.log("test")</script><script>console.log("test")</script></div><div><script>console.log("test")</script> <span>test</span><script>console.log("test")</script></div><div><style>a{color:red}a{color:red}</style></div><div><script>console.log("test")</script><style>a{color:red}</style></div><div><span itemscope><meta itemprop=name content="The Castle">test</span> <span>test</span></div><div><meta name=test></div><div><style>a{color:red}</style></div><div><meta name=test><div>test</div><meta name=test></div><div><meta name=test> <span>test</span><meta name=test></div><svg> <title>test</title> <metadata>test</metadata> <desc>test</desc> </svg><svg> <a>test</a> <a>test</a> </svg><svg><text x=20 y=35><tspan font-weight=bold fill=red>This is bold and red</tspan> <tspan font-weight=bold fill=red>This is bold and red</tspan></text></svg><svg> <tspan>test</tspan><foreignObject>test</foreignObject></svg><svg><text x=20 y=35><tspan font-weight=bold fill=red>This is bold and red</tspan> <tspan font-weight=bold fill=red>This is bold and red</tspan></text></svg><svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice" style=width:100%;height:100%;position:absolute;top:0;left:0;z-index:-1> <linearGradient id=gradient><stop class=begin offset=0% /><stop class=end offset=100% /></linearGradient><rect width=100 height=100 style=fill:url(#gradient) /> <circle cx=50 cy=50 r=30 style=fill:url(#gradient) /> </svg><svg> <script>console.log("test")</script></svg><svg> <style>a{color:red}</style></svg><div><span>test</span> a b <span>test</span> <span>test</span> a b <span>test</span> <span>test</span> a b <span>test</span></div><div><foo-bar> <span>test</span> </foo-bar> <foo-bar> <span>test</span> </foo-bar></div><div><svg> <linearGradient id=gradient /> </svg><span>a</span></div>

1 comment on commit 41093b0

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: 41093b0 Previous: aefc11b Ratio
es/full/minify/libraries/antd 1947116812 ns/iter (± 57266737) 1748109688 ns/iter (± 17480335) 1.11
es/full/minify/libraries/d3 415278392 ns/iter (± 27625194) 363397620 ns/iter (± 9193663) 1.14
es/full/minify/libraries/echarts 1618382624 ns/iter (± 56434405) 1481507695 ns/iter (± 10605396) 1.09
es/full/minify/libraries/jquery 101189389 ns/iter (± 6390662) 94386628 ns/iter (± 1680940) 1.07
es/full/minify/libraries/lodash 110837073 ns/iter (± 4895977) 107645520 ns/iter (± 1616422) 1.03
es/full/minify/libraries/moment 58537819 ns/iter (± 1568351) 54794340 ns/iter (± 826143) 1.07
es/full/minify/libraries/react 20076472 ns/iter (± 996483) 18871283 ns/iter (± 366698) 1.06
es/full/minify/libraries/terser 361825615 ns/iter (± 19836127) 283483870 ns/iter (± 7607016) 1.28
es/full/minify/libraries/three 643517767 ns/iter (± 28558633) 524268008 ns/iter (± 13909328) 1.23
es/full/minify/libraries/typescript 3802784919 ns/iter (± 492403221) 3249166309 ns/iter (± 30481272) 1.17
es/full/minify/libraries/victory 912334212 ns/iter (± 54130646) 771717290 ns/iter (± 11779978) 1.18
es/full/minify/libraries/vue 166430808 ns/iter (± 11248521) 134482843 ns/iter (± 2097866) 1.24
es/full/codegen/es3 35187 ns/iter (± 4671) 32981 ns/iter (± 839) 1.07
es/full/codegen/es5 36162 ns/iter (± 5648) 33087 ns/iter (± 1212) 1.09
es/full/codegen/es2015 37473 ns/iter (± 5488) 33131 ns/iter (± 1707) 1.13
es/full/codegen/es2016 38919 ns/iter (± 7237) 32986 ns/iter (± 1043) 1.18
es/full/codegen/es2017 38312 ns/iter (± 7447) 33028 ns/iter (± 586) 1.16
es/full/codegen/es2018 36559 ns/iter (± 5444) 33112 ns/iter (± 410) 1.10
es/full/codegen/es2019 38168 ns/iter (± 5357) 33275 ns/iter (± 587) 1.15
es/full/codegen/es2020 36801 ns/iter (± 5798) 33022 ns/iter (± 991) 1.11
es/full/all/es3 217099922 ns/iter (± 12824230) 183541605 ns/iter (± 3262227) 1.18
es/full/all/es5 214007170 ns/iter (± 20873001) 174501016 ns/iter (± 2897174) 1.23
es/full/all/es2015 170815819 ns/iter (± 24019257) 140091322 ns/iter (± 3273654) 1.22
es/full/all/es2016 164077194 ns/iter (± 15470486) 139238713 ns/iter (± 3080109) 1.18
es/full/all/es2017 160948136 ns/iter (± 9292730) 138780063 ns/iter (± 3019447) 1.16
es/full/all/es2018 162927717 ns/iter (± 12501176) 136903211 ns/iter (± 4037907) 1.19
es/full/all/es2019 166608397 ns/iter (± 25164250) 136341740 ns/iter (± 3855380) 1.22
es/full/all/es2020 183160135 ns/iter (± 26644117) 132001929 ns/iter (± 2968557) 1.39
es/full/parser 875800 ns/iter (± 131697) 690502 ns/iter (± 23773) 1.27
es/full/base/fixer 27390 ns/iter (± 2853) 24856 ns/iter (± 369) 1.10
es/full/base/resolver_and_hygiene 98030 ns/iter (± 8447) 89474 ns/iter (± 1416) 1.10
serialization of ast node 227 ns/iter (± 36) 204 ns/iter (± 5) 1.11
serialization of serde 213 ns/iter (± 21) 219 ns/iter (± 6) 0.97

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.