Skip to content

Commit

Permalink
feat(html/minifier): Compress style and media attributes (#5022)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed Jun 23, 2022
1 parent 035d1f0 commit 47d34a3
Show file tree
Hide file tree
Showing 17 changed files with 339 additions and 39 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/swc_html_minifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ serde_json = "1.0.61"
swc_atoms = { version = "0.2.9", path = "../swc_atoms" }
swc_cached = { version = "0.1.0", path = "../swc_cached" }
swc_common = { version = "0.18.0", path = "../swc_common" }
swc_css_ast = { version = "0.93.1", path = "../swc_css_ast" }
swc_css_codegen = { version = "0.103.0", path = "../swc_css_codegen" }
swc_css_parser = { version = "0.102.0", path = "../swc_css_parser" }
swc_css_minifier = { version = "0.68.0", path = "../swc_css_minifier" }
Expand Down
200 changes: 187 additions & 13 deletions crates/swc_html_minifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ static COMMA_SEPARATED_HTML_ATTRIBUTES: &[(&str, &str)] = &[
("style", "media"),
];

static COMMA_SEPARATED_SVG_ATTRIBUTES: &[(&str, &str)] = &[("style", "media")];

static SPACE_SEPARATED_GLOBAL_ATTRIBUTES: &[&str] = &[
"class",
"part",
Expand Down Expand Up @@ -251,6 +253,12 @@ enum TextChildrenType {
Module,
}

enum CssMinificationMode {
Stylesheet,
ListOfDeclarations,
MediaQueryList,
}

#[inline(always)]
fn is_whitespace(c: char) -> bool {
matches!(c, '\x09' | '\x0a' | '\x0c' | '\x0d' | '\x20')
Expand Down Expand Up @@ -342,6 +350,9 @@ impl Minifier {
}
_ => COMMA_SEPARATED_HTML_ATTRIBUTES.contains(&(&element.tag_name, attribute_name)),
},
Namespace::SVG => {
COMMA_SEPARATED_SVG_ATTRIBUTES.contains(&(&element.tag_name, attribute_name))
}
_ => false,
}
}
Expand Down Expand Up @@ -837,16 +848,115 @@ impl Minifier {
Some(minified)
}

fn minify_css(&self, data: String) -> Option<String> {
fn minify_sizes(&self, value: &str) -> Option<String> {
let values = value
.rsplitn(2, |c| matches!(c, '\t' | '\n' | '\x0C' | '\r' | ' '))
.collect::<Vec<&str>>();

if values.len() != 2 {
return None;
}

let media_condition =
// It should be `MediaCondition`, but `<media-query> = <media-condition>` and other values is just invalid size
match self.minify_css(values[1].to_string(), CssMinificationMode::MediaQueryList) {
Some(minified) => minified,
_ => return None,
};

let source_size_value = values[0];
let mut minified = String::with_capacity(media_condition.len() + source_size_value.len());

minified.push_str(&media_condition);
minified.push(' ');
minified.push_str(source_size_value);

Some(minified)
}

fn minify_css(&self, data: String, mode: CssMinificationMode) -> Option<String> {
let mut errors: Vec<_> = vec![];

let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
let fm = cm.new_source_file(FileName::Anon, data);

let mut stylesheet = match swc_css_parser::parse_file(&fm, Default::default(), &mut errors)
{
Ok(stylesheet) => stylesheet,
_ => return None,
let mut stylesheet = match mode {
CssMinificationMode::Stylesheet => {
match swc_css_parser::parse_file(&fm, Default::default(), &mut errors) {
Ok(stylesheet) => stylesheet,
_ => return None,
}
}
CssMinificationMode::ListOfDeclarations => {
match swc_css_parser::parse_file::<Vec<swc_css_ast::DeclarationOrAtRule>>(
&fm,
Default::default(),
&mut errors,
) {
Ok(list_of_declarations) => {
let declaration_list: Vec<swc_css_ast::ComponentValue> =
list_of_declarations
.into_iter()
.map(swc_css_ast::ComponentValue::DeclarationOrAtRule)
.collect();

swc_css_ast::Stylesheet {
span: Default::default(),
rules: vec![swc_css_ast::Rule::QualifiedRule(
swc_css_ast::QualifiedRule {
span: Default::default(),
prelude: swc_css_ast::QualifiedRulePrelude::SelectorList(
swc_css_ast::SelectorList {
span: Default::default(),
children: vec![],
},
),
block: swc_css_ast::SimpleBlock {
span: Default::default(),
name: '{',
value: declaration_list,
},
},
)],
}
}
_ => return None,
}
}
CssMinificationMode::MediaQueryList => {
match swc_css_parser::parse_file::<swc_css_ast::MediaQueryList>(
&fm,
Default::default(),
&mut errors,
) {
Ok(media_query_list) => swc_css_ast::Stylesheet {
span: Default::default(),
rules: vec![swc_css_ast::Rule::AtRule(swc_css_ast::AtRule {
span: Default::default(),
name: swc_css_ast::AtRuleName::Ident(swc_css_ast::Ident {
span: Default::default(),
value: "media".into(),
raw: "media".into(),
}),
prelude: Some(swc_css_ast::AtRulePrelude::MediaPrelude(
media_query_list,
)),
block: Some(swc_css_ast::SimpleBlock {
span: Default::default(),
name: '{',
// TODO make the `compress_empty` option for CSS minifier and remove
// it
value: vec![swc_css_ast::ComponentValue::Str(swc_css_ast::Str {
span: Default::default(),
value: "placeholder".into(),
raw: "placeholder".into(),
})],
}),
})],
},
_ => return None,
}
}
};

// Avoid compress potential invalid CSS
Expand All @@ -867,7 +977,41 @@ impl Minifier {
swc_css_codegen::CodegenConfig { minify: true },
);

swc_css_codegen::Emit::emit(&mut gen, &stylesheet).unwrap();
match mode {
CssMinificationMode::Stylesheet => {
swc_css_codegen::Emit::emit(&mut gen, &stylesheet).unwrap();
}
CssMinificationMode::ListOfDeclarations => {
let swc_css_ast::Stylesheet { rules, .. } = &stylesheet;

// Because CSS is grammar free, protect for fails
if let Some(swc_css_ast::Rule::QualifiedRule(swc_css_ast::QualifiedRule {
block,
..
})) = rules.get(0)
{
swc_css_codegen::Emit::emit(&mut gen, &block).unwrap();

minified = minified[1..minified.len() - 1].to_string();
} else {
return None;
}
}
CssMinificationMode::MediaQueryList => {
let swc_css_ast::Stylesheet { rules, .. } = &stylesheet;

// Because CSS is grammar free, protect for fails
if let Some(swc_css_ast::Rule::AtRule(swc_css_ast::AtRule { prelude, .. })) =
rules.get(0)
{
swc_css_codegen::Emit::emit(&mut gen, &prelude).unwrap();

minified = minified.trim().to_string();
} else {
return None;
}
}
}

Some(minified)
}
Expand Down Expand Up @@ -1079,19 +1223,48 @@ impl VisitMut for Minifier {
value = values.join(" ");
} else if self.is_comma_separated_attribute(self.current_element.as_ref().unwrap(), &n.name)
{
let values = value.trim().split(',');
let is_sizes = matches!(&*n.name, "sizes" | "imagesizes");

let mut new_values = vec![];

for value in values {
new_values.push(value.trim());
for value in value.trim().split(',') {
if is_sizes {
let trimmed = value.trim();

match self.minify_sizes(trimmed) {
Some(minified) => {
new_values.push(minified);
}
_ => {
new_values.push(trimmed.to_string());
}
};
} else {
new_values.push(value.trim().to_string());
}
}

value = new_values.join(",");

if self.minify_css && &*n.name == "media" && !value.is_empty() {
if let Some(minified) =
self.minify_css(value.clone(), CssMinificationMode::MediaQueryList)
{
value = minified;
}
}
} else if self
.is_trimable_separated_attribute(self.current_element.as_ref().unwrap(), &n.name)
{
value = value.trim().to_string();

if self.minify_css && &*n.name == "style" && !value.is_empty() {
if let Some(minified) =
self.minify_css(value.clone(), CssMinificationMode::ListOfDeclarations)
{
value = minified;
}
}
} else if is_element_html_namespace && &n.name == "contenteditable" && value == "true" {
n.value = Some(js_word!(""));

Expand Down Expand Up @@ -1253,10 +1426,11 @@ impl VisitMut for Minifier {
n.data = minified.into();
}
Some(TextChildrenType::Css) => {
let minified = match self.minify_css(n.data.to_string()) {
Some(minified) => minified,
None => return,
};
let minified =
match self.minify_css(n.data.to_string(), CssMinificationMode::Stylesheet) {
Some(minified) => minified,
None => return,
};

n.data = minified.into();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@

<header class="lg:px-20 mb-12 mx-auto text-center">
<h2 class="font-bold leading-normal mb-2 text-2xl text-black">What We Do</h2>
<svg version=1.1 x=0px y=0px viewBox="0 0 100 60" style="margin: 0 auto;height: 35px;" xml:space=preserve>
<circle cx=50.1 cy=30.4 r=5 class=stroke-primary style="fill: transparent;stroke-width: 2;stroke-miterlimit: 10;"/>
<line x1=55.1 y1=30.4 x2=100 y2=30.4 class=stroke-primary style="stroke-width: 2;stroke-miterlimit: 10;"/>
<line x1=45.1 y1=30.4 x2=0 y2=30.4 class=stroke-primary style="stroke-width: 2;stroke-miterlimit: 10;"/>
<svg version=1.1 x=0px y=0px viewBox="0 0 100 60" style="margin:0 auto;height:35px" xml:space=preserve>
<circle cx=50.1 cy=30.4 r=5 class=stroke-primary style=fill:transparent;stroke-width:2;stroke-miterlimit:10 />
<line x1=55.1 y1=30.4 x2=100 y2=30.4 class=stroke-primary style=stroke-width:2;stroke-miterlimit:10 />
<line x1=45.1 y1=30.4 x2=0 y2=30.4 class=stroke-primary style=stroke-width:2;stroke-miterlimit:10 />
</svg>
<p class="font-light leading-relaxed mx-auto pb-2 text-gray-500 text-xl">Save time managing advertising & Content for your business.</p>
</header>


<div class="-mx-4 flex flex-row flex-wrap text-center">
<div class="fadeInUp flex-shrink lg:px-6 lg:w-1/3 max-w-full px-4 sm:w-1/2 w-full wow" data-wow-duration=1s style="visibility: visible; animation-duration: 1s; animation-name: fadeInUp;">
<div class="fadeInUp flex-shrink lg:px-6 lg:w-1/3 max-w-full px-4 sm:w-1/2 w-full wow" data-wow-duration=1s style=visibility:visible;animation-duration:1s;animation-name:fadeInUp>

<div class="bg-gray-50 border-b border-gray-100 duration-300 ease-in-out hover:-translate-y-2 mb-12 px-12 py-8 transform transition">
<div class="inline-block mb-4 text-gray-900">
Expand All @@ -42,7 +42,7 @@ <h3 class="font-semibold leading-normal mb-2 text-black text-lg">SEO Services</h
</div>

</div>
<div class="fadeInUp flex-shrink lg:px-6 lg:w-1/3 max-w-full px-4 sm:w-1/2 w-full wow" data-wow-duration=1s data-wow-delay=.1s style="visibility: visible; animation-duration: 1s; animation-delay: 0.1s; animation-name: fadeInUp;">
<div class="fadeInUp flex-shrink lg:px-6 lg:w-1/3 max-w-full px-4 sm:w-1/2 w-full wow" data-wow-duration=1s data-wow-delay=.1s style=visibility:visible;animation-duration:1s;animation-delay:.1s;animation-name:fadeInUp>

<div class="bg-gray-50 border-b border-gray-100 duration-300 ease-in-out hover:-translate-y-2 mb-12 px-12 py-8 transform transition">
<div class="inline-block mb-4 text-gray-900">
Expand All @@ -57,7 +57,7 @@ <h3 class="font-semibold leading-normal mb-2 text-black text-lg">Social Content<
</div>

</div>
<div class="fadeInUp flex-shrink lg:px-6 lg:w-1/3 max-w-full px-4 sm:w-1/2 w-full wow" data-wow-duration=1s data-wow-delay=.3s style="visibility: visible; animation-duration: 1s; animation-delay: 0.3s; animation-name: fadeInUp;">
<div class="fadeInUp flex-shrink lg:px-6 lg:w-1/3 max-w-full px-4 sm:w-1/2 w-full wow" data-wow-duration=1s data-wow-delay=.3s style=visibility:visible;animation-duration:1s;animation-delay:.3s;animation-name:fadeInUp>

<div class="bg-gray-50 border-b border-gray-100 duration-300 ease-in-out hover:-translate-y-2 mb-12 px-12 py-8 transform transition">
<div class="inline-block mb-4 text-gray-900">
Expand All @@ -72,7 +72,7 @@ <h3 class="font-semibold leading-normal mb-2 text-black text-lg">Creative ads</h
</div>

</div>
<div class="fadeInUp flex-shrink lg:px-6 lg:w-1/3 max-w-full px-4 sm:w-1/2 w-full wow" data-wow-duration=1s style="visibility: visible; animation-duration: 1s; animation-name: fadeInUp;">
<div class="fadeInUp flex-shrink lg:px-6 lg:w-1/3 max-w-full px-4 sm:w-1/2 w-full wow" data-wow-duration=1s style=visibility:visible;animation-duration:1s;animation-name:fadeInUp>

<div class="bg-gray-50 border-b border-gray-100 duration-300 ease-in-out hover:-translate-y-2 mb-12 px-12 py-8 transform transition">
<div class="inline-block mb-4 text-gray-900">
Expand All @@ -87,7 +87,7 @@ <h3 class="font-semibold leading-normal mb-2 text-black text-lg">Brand Identity<
</div>

</div>
<div class="fadeInUp flex-shrink lg:px-6 lg:w-1/3 max-w-full px-4 sm:w-1/2 w-full wow" data-wow-duration=1s data-wow-delay=.1s style="visibility: visible; animation-duration: 1s; animation-delay: 0.1s; animation-name: fadeInUp;">
<div class="fadeInUp flex-shrink lg:px-6 lg:w-1/3 max-w-full px-4 sm:w-1/2 w-full wow" data-wow-duration=1s data-wow-delay=.1s style=visibility:visible;animation-duration:1s;animation-delay:.1s;animation-name:fadeInUp>

<div class="bg-gray-50 border-b border-gray-100 duration-300 ease-in-out hover:-translate-y-2 mb-12 px-12 py-8 transform transition">
<div class="inline-block mb-4 text-gray-900">
Expand All @@ -101,7 +101,7 @@ <h3 class="font-semibold leading-normal mb-2 text-black text-lg">Budget & Market
</div>

</div>
<div class="fadeInUp flex-shrink lg:px-6 lg:w-1/3 max-w-full px-4 sm:w-1/2 w-full wow" data-wow-duration=1s data-wow-delay=.3s style="visibility: visible; animation-duration: 1s; animation-delay: 0.3s; animation-name: fadeInUp;">
<div class="fadeInUp flex-shrink lg:px-6 lg:w-1/3 max-w-full px-4 sm:w-1/2 w-full wow" data-wow-duration=1s data-wow-delay=.3s style=visibility:visible;animation-duration:1s;animation-delay:.3s;animation-name:fadeInUp>

<div class="bg-gray-50 border-b border-gray-100 duration-300 ease-in-out hover:-translate-y-2 mb-12 px-12 py-8 transform transition">
<div class="inline-block mb-4 text-gray-900">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!doctype html><html lang=en><title>Document</title><link rel=preload as=image imagesrcset="dog-cropped-1x.jpg,dog-cropped-2x.jpg 2x" media="(max-width: 800px)"><link rel=preload as=image imagesrcset="dog-wide-1x.jpg,dog-wide-2x.jpg 2x" media="(min-width: 801px)"><link rel=preload as=image imagesrcset="wolf_400px.jpg 400w,wolf_800px.jpg 800w,wolf_1600px.jpg 1600w" imagesizes=50vw><link rel=preload as=image imagesrcset="wolf_400px.jpg 400w,wolf_800px.jpg 800w,wolf_1600px.jpg 1600w" imagesizes="(max-width: 600px) 480px,800px"><body>
<!doctype html><html lang=en><title>Document</title><link rel=preload as=image imagesrcset="dog-cropped-1x.jpg,dog-cropped-2x.jpg 2x" media=(max-width:800px)><link rel=preload as=image imagesrcset="dog-wide-1x.jpg,dog-wide-2x.jpg 2x" media=(min-width:801px)><link rel=preload as=image imagesrcset="wolf_400px.jpg 400w,wolf_800px.jpg 800w,wolf_1600px.jpg 1600w" imagesizes=50vw><link rel=preload as=image imagesrcset="wolf_400px.jpg 400w,wolf_800px.jpg 800w,wolf_1600px.jpg 1600w" imagesizes="(max-width:600px) 480px,800px"><body>
<div>test</div>
<img srcset="elva-fairy-480w.jpg 480w,elva-fairy-800w.jpg 800w" sizes="(max-width: 600px) 480px,800px" src=elva-fairy-800w.jpg alt="Elva dressed as a fairy">
<img srcset="elva-fairy-480w.jpg 480w,elva-fairy-800w.jpg 800w" sizes="(max-width:600px) 480px,800px" src=elva-fairy-800w.jpg alt="Elva dressed as a fairy">

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<div type=text onmouseover=myFunction()>test</div>
<div type=text onmouseover=myFunction()>test</div>
<div type=text>test</div>
<svg version=1.1 viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice" style="width:100%; height:100%; position:absolute; top:0; left:0; z-index:-1;">
<svg version=1.1 viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice" style=width:100%;height:100%;position:absolute;top:0;left:0;z-index:-1>
<circle onmouseover='alert("test")' cx=50 cy=50 r=30 style=fill:url(#gradient) />
</svg>

1 comment on commit 47d34a3

@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: 47d34a3 Previous: a057183 Ratio
es/full/minify/libraries/antd 1610173836 ns/iter (± 9261193) 1610325828 ns/iter (± 54053710) 1.00
es/full/minify/libraries/d3 407066465 ns/iter (± 6123127) 416841370 ns/iter (± 22310404) 0.98
es/full/minify/libraries/echarts 1595742452 ns/iter (± 17988364) 1662970280 ns/iter (± 75009676) 0.96
es/full/minify/libraries/jquery 87787634 ns/iter (± 863458) 100408295 ns/iter (± 4626616) 0.87
es/full/minify/libraries/lodash 115675915 ns/iter (± 1536439) 124251445 ns/iter (± 7145215) 0.93
es/full/minify/libraries/moment 51180371 ns/iter (± 661328) 51237864 ns/iter (± 2542162) 1.00
es/full/minify/libraries/react 16989376 ns/iter (± 388476) 17600180 ns/iter (± 745158) 0.97
es/full/minify/libraries/terser 599010362 ns/iter (± 9625268) 605840072 ns/iter (± 16119932) 0.99
es/full/minify/libraries/three 535861197 ns/iter (± 4201618) 541267793 ns/iter (± 26526228) 0.99
es/full/minify/libraries/typescript 3400263690 ns/iter (± 61791998) 3421016946 ns/iter (± 73295386) 0.99
es/full/minify/libraries/victory 730068947 ns/iter (± 8573472) 712595102 ns/iter (± 12038432) 1.02
es/full/minify/libraries/vue 144714239 ns/iter (± 8327625) 137678276 ns/iter (± 7237265) 1.05
es/full/codegen/es3 32009 ns/iter (± 1304) 32601 ns/iter (± 1350) 0.98
es/full/codegen/es5 31796 ns/iter (± 788) 32647 ns/iter (± 1190) 0.97
es/full/codegen/es2015 32073 ns/iter (± 606) 32680 ns/iter (± 1478) 0.98
es/full/codegen/es2016 31995 ns/iter (± 805) 32716 ns/iter (± 1499) 0.98
es/full/codegen/es2017 31991 ns/iter (± 1204) 32695 ns/iter (± 1589) 0.98
es/full/codegen/es2018 32073 ns/iter (± 894) 32690 ns/iter (± 883) 0.98
es/full/codegen/es2019 32001 ns/iter (± 926) 32750 ns/iter (± 1136) 0.98
es/full/codegen/es2020 31994 ns/iter (± 1064) 32771 ns/iter (± 1645) 0.98
es/full/all/es3 193491453 ns/iter (± 7701445) 196762948 ns/iter (± 14704724) 0.98
es/full/all/es5 184145035 ns/iter (± 5902906) 180214055 ns/iter (± 10390608) 1.02
es/full/all/es2015 148279095 ns/iter (± 4941489) 138450416 ns/iter (± 4116063) 1.07
es/full/all/es2016 147838205 ns/iter (± 5836716) 141554798 ns/iter (± 9190769) 1.04
es/full/all/es2017 136721861 ns/iter (± 4505631) 144106800 ns/iter (± 8529000) 0.95
es/full/all/es2018 134826393 ns/iter (± 1705581) 146194583 ns/iter (± 10505526) 0.92
es/full/all/es2019 134825798 ns/iter (± 2694512) 145796178 ns/iter (± 9423319) 0.92
es/full/all/es2020 133130665 ns/iter (± 5208024) 137662275 ns/iter (± 8368571) 0.97
es/full/parser 711716 ns/iter (± 14330) 723414 ns/iter (± 26713) 0.98
es/full/base/fixer 29697 ns/iter (± 481) 29755 ns/iter (± 1074) 1.00
es/full/base/resolver_and_hygiene 87794 ns/iter (± 2389) 87813 ns/iter (± 8247) 1.00
serialization of ast node 206 ns/iter (± 4) 205 ns/iter (± 1) 1.00
serialization of serde 218 ns/iter (± 3) 217 ns/iter (± 1) 1.00

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

Please sign in to comment.