Skip to content

Commit

Permalink
feat(html/minifier): Improve removal of redundant attributes (#6197)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed Oct 20, 2022
1 parent 7d5b544 commit aa3fab1
Show file tree
Hide file tree
Showing 34 changed files with 368 additions and 113 deletions.
150 changes: 112 additions & 38 deletions crates/swc_html_minifier/src/lib.rs
Expand Up @@ -18,7 +18,7 @@ use swc_html_visit::{VisitMut, VisitMutWith};

use crate::option::{
CollapseWhitespaces, CssOptions, JsOptions, JsParserOptions, JsonOptions, MinifierType,
MinifyCssOption, MinifyJsOption, MinifyJsonOption, MinifyOptions,
MinifyCssOption, MinifyJsOption, MinifyJsonOption, MinifyOptions, RemoveRedundantAttributes,
};

pub mod option;
Expand Down Expand Up @@ -563,44 +563,82 @@ impl Minifier<'_> {

match namespace {
Namespace::HTML | Namespace::SVG => {
// Legacy attributes, not in spec
if *tag_name == js_word!("script") {
match attribute.name {
js_word!("type") => {
let value = if let Some(next) = attribute_value.split(';').next() {
next
} else {
attribute_value
};

match value {
// Legacy JavaScript MIME types
"application/javascript"
| "application/ecmascript"
| "application/x-ecmascript"
| "application/x-javascript"
| "text/ecmascript"
| "text/javascript1.0"
| "text/javascript1.1"
| "text/javascript1.2"
| "text/javascript1.3"
| "text/javascript1.4"
| "text/javascript1.5"
| "text/jscript"
| "text/livescript"
| "text/x-ecmascript"
| "text/x-javascript" => return true,
_ => {}
match *tag_name {
js_word!("html") => match attribute.name {
js_word!("xmlns") => {
if &*attribute_value.trim().to_ascii_lowercase()
== "http://www.w3.org/1999/xhtml"
{
return true;
}
}
js_word!("xmlns:xlink") => {
if &*attribute_value.trim().to_ascii_lowercase()
== "http://www.w3.org/1999/xlink"
{
return true;
}
}
js_word!("language") => match &*attribute_value.trim().to_ascii_lowercase()
{
"javascript" | "javascript1.2" | "javascript1.3" | "javascript1.4"
| "javascript1.5" | "javascript1.6" | "javascript1.7" => return true,
_ => {}
},
_ => {}
},
js_word!("script") => {
match attribute.name {
js_word!("type") => {
let value = if let Some(next) = attribute_value.split(';').next() {
next
} else {
attribute_value
};

match value {
// Legacy JavaScript MIME types
"application/javascript"
| "application/ecmascript"
| "application/x-ecmascript"
| "application/x-javascript"
| "text/ecmascript"
| "text/javascript1.0"
| "text/javascript1.1"
| "text/javascript1.2"
| "text/javascript1.3"
| "text/javascript1.4"
| "text/javascript1.5"
| "text/jscript"
| "text/livescript"
| "text/x-ecmascript"
| "text/x-javascript" => return true,
"text/javascript" => return true,
_ => {}
}
}
js_word!("language") => {
match &*attribute_value.trim().to_ascii_lowercase() {
"javascript" | "javascript1.2" | "javascript1.3"
| "javascript1.4" | "javascript1.5" | "javascript1.6"
| "javascript1.7" => return true,
_ => {}
}
}
_ => {}
}
}
js_word!("link") => {
if attribute.name == js_word!("type")
&& &*attribute_value.trim().to_ascii_lowercase() == "text/css"
{
return true;
}
}

js_word!("svg") => {
if attribute.name == js_word!("xmlns")
&& &*attribute_value.trim().to_ascii_lowercase()
== "http://www.w3.org/2000/svg"
{
return true;
}
}
_ => {}
}

let default_attributes = if namespace == Namespace::HTML {
Expand Down Expand Up @@ -635,7 +673,43 @@ impl Minifier<'_> {

match (attribute_info.inherited, &attribute_info.initial) {
(None, Some(initial)) | (Some(false), Some(initial)) => {
initial == normalized_value
match self.options.remove_redundant_attributes {
RemoveRedundantAttributes::None => false,
RemoveRedundantAttributes::Smart => {
if initial == normalized_value {
// It is safe to remove deprecated redundant attributes, they
// should not be used
if attribute_info.deprecated == Some(true) {
return true;
}

// It it safe to remove svg redundant attributes, they used for
// styling
if namespace == Namespace::SVG {
return true;
}

// It it safe to remove redundant attributes for metadata
// elements
if namespace == Namespace::HTML
&& matches!(
*tag_name,
js_word!("base")
| js_word!("link")
| js_word!("noscript")
| js_word!("script")
| js_word!("style")
| js_word!("title")
)
{
return true;
}
}

false
}
RemoveRedundantAttributes::All => initial == normalized_value,
}
}
_ => false,
}
Expand Down Expand Up @@ -1607,7 +1681,7 @@ impl Minifier<'_> {
let result = child_will_be_retained(&mut child, &mut new_children, children);

if result {
if self.options.remove_redundant_attributes
if self.options.remove_empty_metadata_elements
&& self.is_empty_metadata_element(&child)
{
let need_continue = {
Expand Down Expand Up @@ -2242,7 +2316,7 @@ impl VisitMut for Minifier<'_> {

for (i, i1) in n.attributes.iter().enumerate() {
if i1.value.is_some() {
if self.options.remove_redundant_attributes
if self.options.remove_redundant_attributes != RemoveRedundantAttributes::None
&& self.is_default_attribute_value(n.namespace, &n.tag_name, i1)
{
remove_list.push(i);
Expand Down
36 changes: 29 additions & 7 deletions crates/swc_html_minifier/src/option.rs
Expand Up @@ -44,6 +44,32 @@ pub enum CollapseWhitespaces {
OnlyMetadata,
}

impl Default for CollapseWhitespaces {
fn default() -> Self {
CollapseWhitespaces::OnlyMetadata
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "kebab-case")]
pub enum RemoveRedundantAttributes {
/// Do not remove redundant attributes
None,
/// Remove all redundant attributes
All,
/// Remove deprecated and svg redundant (they used for styling) and `xmlns`
/// attributes (for example the `type` attribute for the `style` tag and
/// `xmlns` for svg)
Smart,
}

impl Default for RemoveRedundantAttributes {
fn default() -> Self {
RemoveRedundantAttributes::Smart
}
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
Expand Down Expand Up @@ -116,7 +142,7 @@ pub struct CssOptions {
pub struct MinifyOptions {
#[serde(default)]
pub force_set_html5_doctype: bool,
#[serde(default = "default_collapse_whitespaces")]
#[serde(default)]
pub collapse_whitespaces: CollapseWhitespaces,
// Remove safe empty elements with metadata content, i.e. the `script` and `style` element
// without content and attributes, `meta` and `link` elements without attributes and etc
Expand All @@ -136,8 +162,8 @@ pub struct MinifyOptions {
/// libraries
#[serde(default = "true_by_default")]
pub remove_empty_attributes: bool,
#[serde(default = "true_by_default")]
pub remove_redundant_attributes: bool,
#[serde(default)]
pub remove_redundant_attributes: RemoveRedundantAttributes,
#[serde(default = "true_by_default")]
pub collapse_boolean_attributes: bool,
/// Merge the same metadata elements into one (for example, consecutive
Expand Down Expand Up @@ -184,10 +210,6 @@ impl Default for MinifyOptions {
}
}

const fn default_collapse_whitespaces() -> CollapseWhitespaces {
CollapseWhitespaces::OnlyMetadata
}

const fn true_by_default() -> bool {
true
}
Expand Down
Expand Up @@ -19,7 +19,7 @@
<ul compact></ul>
<video src="" controls autoplay></video>
<script async defer></script>
<input autofocus checked disabled>
<input type=text autofocus checked disabled>
<button formnovalidate></button>
<div allowfullscreen async autofocus autoplay checked compact controls declare default defaultchecked defaultmuted defaultselected defer disabled enabled formnovalidate hidden indeterminate inert ismap itemscope loop multiple muted nohref noresize noshade novalidate nowrap open pauseonexit readonly required reversed scoped seamless selected sortable truespeed typemustmatch visible></div>

Expand Down
@@ -1,9 +1,9 @@
<!doctype html><html lang=en><title>Document</title><table width=200 border=1 align=center cellpadding=4 cellspacing=0>
<tr>
<th scope=col>Cell 1</th>
<th colspan=1 rowspan=1 scope=col>Cell 1</th>
</tr>
<tr>
<td class=test bgcolor=#FBF0DB>
<td class=test colspan=1 rowspan=1 bgcolor=#FBF0DB>
Cell 1
</td>
</tr>
Expand Down
@@ -1 +1 @@
<!doctype html><html lang=en><title>Document</title><input class=form-control id={{vm.formInputName}} name={{vm.formInputName}} placeholder=YYYY-MM-DD date-range-picker data-ng-model=vm.value data-ng-model-options="{ debounce: 1000 }" data-ng-pattern=vm.options.format data-options=vm.datepickerOptions>
<!doctype html><html lang=en><title>Document</title><input class=form-control type=text id={{vm.formInputName}} name={{vm.formInputName}} placeholder=YYYY-MM-DD date-range-picker data-ng-model=vm.value data-ng-model-options="{ debounce: 1000 }" data-ng-pattern=vm.options.format data-options=vm.datepickerOptions>

This file was deleted.

This file was deleted.

This file was deleted.

@@ -1,10 +1,10 @@
<!doctype html><meta charset=utf-8><title>Test</title><form action=handler.php method=post>
<input name=str>
<input type=text name=str>
<input type=submit value=send>
</form>
<form action=handler.php>
<input name=str>
<form action=handler.php method=get>
<input type=text name=str>
<input type=submit value=send>
<input value=foo>
<input type=text value=foo>
<input type=checkbox>
</form>
@@ -1 +1 @@
<!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"><meta http-equiv=X-UA-Compatible content="ie=edge"><title>Document</title><iframe id=test src=test.html></iframe>
<!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"><meta http-equiv=X-UA-Compatible content="ie=edge"><title>Document</title><iframe id=test src=test.html height=150 width=300 loading=eager fetchpriority=auto referrerpolicy=strict-origin-when-cross-origin></iframe>
@@ -1 +1 @@
<!doctype html><html lang=en><title>Document</title><img src=test.png alt=test>
<!doctype html><html lang=en><title>Document</title><img src=test.png alt=test decoding=auto loading=eager referrerpolicy=strict-origin-when-cross-origin>
@@ -1,4 +1,4 @@
<!doctype html><html lang=en><title>Document</title><input name=a>
<input name=b>
<input name=c>
<input name=d>
<!doctype html><html lang=en><title>Document</title><input name=a type=text>
<input name=b type=text>
<input name=c type=text>
<input name=d size=20>
@@ -1,5 +1,5 @@
<!doctype html><html lang=en><title>Document</title><form action=/test>
<input onkeydown=myFunction()>
<input type=text onkeydown=myFunction()>
</form>
<div type=text onmouseover=myFunction()>test</div>
<div type=text onmouseover=myFunction()>test</div>
Expand Down
@@ -1,3 +1,3 @@
<!doctype html><html lang=en><title>Document</title><meter id=fuel max=100 low=33 high=66 optimum=80 value=50>
<!doctype html><html lang=en><title>Document</title><meter id=fuel min=0 max=100 low=33 high=66 optimum=80 value=50>
at 50/100
</meter>
@@ -1,11 +1,11 @@
<!doctype html><html lang=en><title>Document</title><ol>
<!doctype html><html lang=en><title>Document</title><ol type=1>
<li>Mix flour, baking powder, sugar, and salt.</li>
<li>In another bowl, mix eggs, milk, and oil.</li>
<li>Stir both mixtures together.</li>
<li>Fill muffin tray 3/4 full.</li>
<li>Bake for 20 minutes.</li>
</ol>
<ol>
<ol type=1 start=1>
<li>United Kingdom
<li>Switzerland
<li>United States
Expand Down
@@ -1 +1 @@
<!doctype html><html lang=en><title>Document</title><progress></progress>
<!doctype html><html lang=en><title>Document</title><progress max=1></progress>
@@ -0,0 +1,3 @@
{
"removeRedundantAttributes": "all"
}

1 comment on commit aa3fab1

@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: aa3fab1 Previous: ba5d272 Ratio
es/full/bugs-1 436307 ns/iter (± 32385) 342019 ns/iter (± 151871) 1.28
es/full/minify/libraries/antd 2126277437 ns/iter (± 66544545) 1807406436 ns/iter (± 34847259) 1.18
es/full/minify/libraries/d3 429381788 ns/iter (± 11305454) 456884850 ns/iter (± 23743350) 0.94
es/full/minify/libraries/echarts 1812696108 ns/iter (± 70089579) 1692449979 ns/iter (± 59458825) 1.07
es/full/minify/libraries/jquery 173313745 ns/iter (± 101346846) 107594006 ns/iter (± 5983772) 1.61
es/full/minify/libraries/lodash 143424466 ns/iter (± 4605003) 149364661 ns/iter (± 58355404) 0.96
es/full/minify/libraries/moment 71211711 ns/iter (± 5020616) 67800793 ns/iter (± 3895890) 1.05
es/full/minify/libraries/react 25783015 ns/iter (± 2638855) 22283238 ns/iter (± 1452895) 1.16
es/full/minify/libraries/terser 346419831 ns/iter (± 23592199) 374569879 ns/iter (± 27228888) 0.92
es/full/minify/libraries/three 625352795 ns/iter (± 40312972) 588540350 ns/iter (± 19794071) 1.06
es/full/minify/libraries/typescript 4148999821 ns/iter (± 117806755) 3669124134 ns/iter (± 142810842) 1.13
es/full/minify/libraries/victory 917375856 ns/iter (± 31948624) 880724365 ns/iter (± 43190149) 1.04
es/full/minify/libraries/vue 187323170 ns/iter (± 22261199) 219084543 ns/iter (± 27156893) 0.86
es/full/codegen/es3 42938 ns/iter (± 1727) 41994 ns/iter (± 9234) 1.02
es/full/codegen/es5 42750 ns/iter (± 5070) 39247 ns/iter (± 7007) 1.09
es/full/codegen/es2015 42222 ns/iter (± 2050) 38428 ns/iter (± 8021) 1.10
es/full/codegen/es2016 42368 ns/iter (± 3179) 36191 ns/iter (± 5883) 1.17
es/full/codegen/es2017 42892 ns/iter (± 2919) 35237 ns/iter (± 6599) 1.22
es/full/codegen/es2018 43464 ns/iter (± 7032) 37290 ns/iter (± 6910) 1.17
es/full/codegen/es2019 42526 ns/iter (± 1851) 39846 ns/iter (± 7687) 1.07
es/full/codegen/es2020 43033 ns/iter (± 2202) 35363 ns/iter (± 6915) 1.22
es/full/all/es3 242975838 ns/iter (± 23480303) 253014717 ns/iter (± 30814215) 0.96
es/full/all/es5 230817583 ns/iter (± 16504197) 241473716 ns/iter (± 21648720) 0.96
es/full/all/es2015 181934898 ns/iter (± 14632648) 195235157 ns/iter (± 22187668) 0.93
es/full/all/es2016 181028295 ns/iter (± 15751721) 192246322 ns/iter (± 26840842) 0.94
es/full/all/es2017 182751339 ns/iter (± 9941067) 167516691 ns/iter (± 16704808) 1.09
es/full/all/es2018 181901217 ns/iter (± 14655668) 165054735 ns/iter (± 21579972) 1.10
es/full/all/es2019 178134235 ns/iter (± 18258760) 160245488 ns/iter (± 9273239) 1.11
es/full/all/es2020 174217115 ns/iter (± 17574202) 184429445 ns/iter (± 25090698) 0.94
es/full/parser 932897 ns/iter (± 133091) 906299 ns/iter (± 383157) 1.03
es/full/base/fixer 32752 ns/iter (± 860) 27252 ns/iter (± 3395) 1.20
es/full/base/resolver_and_hygiene 118428 ns/iter (± 4266) 101271 ns/iter (± 14105) 1.17
serialization of ast node 263 ns/iter (± 24) 261 ns/iter (± 38) 1.01
serialization of serde 272 ns/iter (± 29) 240 ns/iter (± 24) 1.13

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

Please sign in to comment.