Fix [style] and script[type] minification; optimise attr ordering; refactor bench runners
This commit is contained in:
parent
f29ae39e47
commit
5cef4219e6
|
@ -0,0 +1,2 @@
|
||||||
|
/package-lock.json
|
||||||
|
node_modules/
|
|
@ -1,32 +1,9 @@
|
||||||
const fs = require("fs");
|
|
||||||
const minifyHtml = require("@minify-html/js");
|
const minifyHtml = require("@minify-html/js");
|
||||||
const path = require("path");
|
const { htmlOnly, run } = require("../common");
|
||||||
|
|
||||||
const iterations = parseInt(process.env.MHB_ITERATIONS, 10);
|
|
||||||
const inputDir = process.env.MHB_INPUT_DIR;
|
|
||||||
const htmlOnly = process.env.MHB_HTML_ONLY === "1";
|
|
||||||
const outputDir = process.env.MHB_OUTPUT_DIR;
|
|
||||||
|
|
||||||
const minifyHtmlCfg = minifyHtml.createConfiguration({
|
const minifyHtmlCfg = minifyHtml.createConfiguration({
|
||||||
minify_css: !htmlOnly,
|
minify_css: !htmlOnly,
|
||||||
minify_js: !htmlOnly,
|
minify_js: !htmlOnly,
|
||||||
});
|
});
|
||||||
|
|
||||||
const results = fs.readdirSync(inputDir).map((name) => {
|
run((src) => minifyHtml.minify(src, minifyHtmlCfg));
|
||||||
const src = fs.readFileSync(path.join(inputDir, name));
|
|
||||||
|
|
||||||
const out = minifyHtml.minify(src, minifyHtmlCfg);
|
|
||||||
const len = out.byteLength;
|
|
||||||
if (outputDir) {
|
|
||||||
fs.writeFileSync(path.join(outputDir, name), out);
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = process.hrtime.bigint();
|
|
||||||
for (let i = 0; i < iterations; i++) {
|
|
||||||
minifyHtml.minify(src, minifyHtmlCfg);
|
|
||||||
}
|
|
||||||
const elapsed = process.hrtime.bigint() - start;
|
|
||||||
|
|
||||||
return [name, len, iterations, Number(elapsed) / 1_000_000_000];
|
|
||||||
});
|
|
||||||
console.log(JSON.stringify(results));
|
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
const esbuild = require("esbuild");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const iterations = parseInt(process.env.MHB_ITERATIONS, 10);
|
||||||
|
const inputDir = process.env.MHB_INPUT_DIR;
|
||||||
|
const htmlOnly = process.env.MHB_HTML_ONLY === "1";
|
||||||
|
const outputDir = process.env.MHB_OUTPUT_DIR;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
htmlOnly,
|
||||||
|
|
||||||
|
esbuildCss: (code, type) => {
|
||||||
|
if (type === "inline") {
|
||||||
|
code = `x{${code}}`;
|
||||||
|
}
|
||||||
|
code = esbuild.transformSync(code, {
|
||||||
|
loader: "css",
|
||||||
|
minify: true,
|
||||||
|
}).code;
|
||||||
|
if (type === "inline") {
|
||||||
|
code = code.slice(2, -1);
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
},
|
||||||
|
|
||||||
|
esbuildJs: (code) =>
|
||||||
|
esbuild.transformSync(code, {
|
||||||
|
loader: "js",
|
||||||
|
minify: true,
|
||||||
|
}).code,
|
||||||
|
|
||||||
|
run: (minifierFn) => {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
fs.readdirSync(inputDir).map((name) => {
|
||||||
|
const src = fs.readFileSync(path.join(inputDir, name), "utf8");
|
||||||
|
|
||||||
|
const out = minifierFn(src);
|
||||||
|
const len = Buffer.from(out).length;
|
||||||
|
if (outputDir) {
|
||||||
|
fs.writeFileSync(path.join(outputDir, name), out);
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = process.hrtime.bigint();
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
minifierFn(src);
|
||||||
|
}
|
||||||
|
const elapsed = process.hrtime.bigint() - start;
|
||||||
|
|
||||||
|
return [name, len, iterations, Number(elapsed) / 1_000_000_000];
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,24 +1,5 @@
|
||||||
const esbuild = require("esbuild");
|
|
||||||
const fs = require("fs");
|
|
||||||
const htmlMinifier = require("html-minifier");
|
const htmlMinifier = require("html-minifier");
|
||||||
const path = require("path");
|
const { htmlOnly, esbuildCss, esbuildJs, run } = require("../common");
|
||||||
|
|
||||||
const iterations = parseInt(process.env.MHB_ITERATIONS, 10);
|
|
||||||
const inputDir = process.env.MHB_INPUT_DIR;
|
|
||||||
const htmlOnly = process.env.MHB_HTML_ONLY === "1";
|
|
||||||
const outputDir = process.env.MHB_OUTPUT_DIR;
|
|
||||||
|
|
||||||
const esbuildCss = (code) =>
|
|
||||||
esbuild.transformSync(code, {
|
|
||||||
loader: "css",
|
|
||||||
minify: true,
|
|
||||||
}).code;
|
|
||||||
|
|
||||||
const esbuildJs = (code) =>
|
|
||||||
esbuild.transformSync(code, {
|
|
||||||
loader: "js",
|
|
||||||
minify: true,
|
|
||||||
}).code;
|
|
||||||
|
|
||||||
const htmlMinifierCfg = {
|
const htmlMinifierCfg = {
|
||||||
collapseBooleanAttributes: true,
|
collapseBooleanAttributes: true,
|
||||||
|
@ -46,21 +27,4 @@ const htmlMinifierCfg = {
|
||||||
useShortDoctype: true,
|
useShortDoctype: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const results = fs.readdirSync(inputDir).map((name) => {
|
run((src) => htmlMinifier.minify(src, htmlMinifierCfg));
|
||||||
const src = fs.readFileSync(path.join(inputDir, name), "utf8");
|
|
||||||
|
|
||||||
const out = htmlMinifier.minify(src, htmlMinifierCfg);
|
|
||||||
const len = Buffer.from(out, "utf8").length;
|
|
||||||
if (outputDir) {
|
|
||||||
fs.writeFileSync(path.join(outputDir, name), out);
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = process.hrtime.bigint();
|
|
||||||
for (let i = 0; i < iterations; i++) {
|
|
||||||
htmlMinifier.minify(src, htmlMinifierCfg);
|
|
||||||
}
|
|
||||||
const elapsed = process.hrtime.bigint() - start;
|
|
||||||
|
|
||||||
return [name, len, iterations, Number(elapsed) / 1_000_000_000];
|
|
||||||
});
|
|
||||||
console.log(JSON.stringify(results));
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "0.12.19",
|
|
||||||
"html-minifier": "4.0.0"
|
"html-minifier": "4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
const esbuild = require("esbuild");
|
|
||||||
const fs = require("fs");
|
|
||||||
const minimize = require("minimize");
|
const minimize = require("minimize");
|
||||||
const path = require("path");
|
const { htmlOnly, esbuildCss, esbuildJs, run } = require("../common");
|
||||||
|
|
||||||
const iterations = parseInt(process.env.MHB_ITERATIONS, 10);
|
|
||||||
const inputDir = process.env.MHB_INPUT_DIR;
|
|
||||||
const htmlOnly = process.env.MHB_HTML_ONLY === "1";
|
|
||||||
const outputDir = process.env.MHB_OUTPUT_DIR;
|
|
||||||
|
|
||||||
const jsMime = new Set([
|
const jsMime = new Set([
|
||||||
undefined,
|
undefined,
|
||||||
|
@ -28,18 +21,6 @@ const jsMime = new Set([
|
||||||
"text/x-javascript",
|
"text/x-javascript",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const esbuildCss = (code) =>
|
|
||||||
esbuild.transformSync(code, {
|
|
||||||
loader: "css",
|
|
||||||
minify: true,
|
|
||||||
}).code;
|
|
||||||
|
|
||||||
const esbuildJs = (code) =>
|
|
||||||
esbuild.transformSync(code, {
|
|
||||||
loader: "js",
|
|
||||||
minify: true,
|
|
||||||
}).code;
|
|
||||||
|
|
||||||
const jsCssPlugin = {
|
const jsCssPlugin = {
|
||||||
id: "esbuild",
|
id: "esbuild",
|
||||||
element: (node, next) => {
|
element: (node, next) => {
|
||||||
|
@ -61,21 +42,4 @@ const plugins = htmlOnly ? [] : [jsCssPlugin];
|
||||||
|
|
||||||
const minifier = new minimize({ plugins });
|
const minifier = new minimize({ plugins });
|
||||||
|
|
||||||
const results = fs.readdirSync(inputDir).map((name) => {
|
run((src) => minifier.parse(src));
|
||||||
const src = fs.readFileSync(path.join(inputDir, name), "utf8");
|
|
||||||
|
|
||||||
const out = minifier.parse(src);
|
|
||||||
const len = Buffer.from(out, "utf8").length;
|
|
||||||
if (outputDir) {
|
|
||||||
fs.writeFileSync(path.join(outputDir, name), out);
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = process.hrtime.bigint();
|
|
||||||
for (let i = 0; i < iterations; i++) {
|
|
||||||
minifier.parse(src);
|
|
||||||
}
|
|
||||||
const elapsed = process.hrtime.bigint() - start;
|
|
||||||
|
|
||||||
return [name, len, iterations, Number(elapsed) / 1_000_000_000];
|
|
||||||
});
|
|
||||||
console.log(JSON.stringify(results));
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "0.12.19",
|
|
||||||
"minimize": "2.2.0"
|
"minimize": "2.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "0.12.19"
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ mkdir -p "$output_dir"
|
||||||
|
|
||||||
pushd "../../bench/runners" >/dev/null
|
pushd "../../bench/runners" >/dev/null
|
||||||
for r in *; do
|
for r in *; do
|
||||||
if [ ! -d "$r" ]; then
|
if [[ ! -d "$r" ]] || [[ "$r" == "node_modules" ]]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
mkdir -p "$output_dir/$r"
|
mkdir -p "$output_dir/$r"
|
||||||
|
|
2
format
2
format
|
@ -4,7 +4,7 @@ set -Eeuxo pipefail
|
||||||
|
|
||||||
pushd "$(dirname "$0")" >/dev/null
|
pushd "$(dirname "$0")" >/dev/null
|
||||||
|
|
||||||
npx prettier@2.3.2 -w 'version' 'bench/*.{js,json}' 'bench/*/*.{js,json}' 'gen/*.{ts,json}'
|
npx prettier@2.3.2 -w 'version' 'bench/*.{js,json}' 'bench/runners/*.{js,json}' 'bench/runners/*/*.{js,json}' 'gen/*.{ts,json}'
|
||||||
|
|
||||||
for dir in \
|
for dir in \
|
||||||
bench/runners/minify-html \
|
bench/runners/minify-html \
|
||||||
|
|
|
@ -22,6 +22,6 @@ js-esbuild = ["crossbeam", "esbuild-rs"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aho-corasick = "0.7"
|
aho-corasick = "0.7"
|
||||||
crossbeam = { version = "0.7", optional = true }
|
crossbeam = { version = "0.7", optional = true }
|
||||||
esbuild-rs = { version = "0.12.18", optional = true }
|
esbuild-rs = { version = "0.12.19", optional = true }
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
memchr = "2"
|
memchr = "2"
|
||||||
|
|
|
@ -326,7 +326,7 @@ pub fn minify_attr(
|
||||||
if (value_raw.is_empty() && redundant_if_empty)
|
if (value_raw.is_empty() && redundant_if_empty)
|
||||||
|| default_value.filter(|dv| dv == &value_raw).is_some()
|
|| default_value.filter(|dv| dv == &value_raw).is_some()
|
||||||
// TODO Cfg.
|
// TODO Cfg.
|
||||||
|| (tag == b"script" && JAVASCRIPT_MIME_TYPES.contains(value_raw.as_slice()))
|
|| (tag == b"script" && name == b"type" && JAVASCRIPT_MIME_TYPES.contains(value_raw.as_slice()))
|
||||||
{
|
{
|
||||||
return AttrMinified::Redundant;
|
return AttrMinified::Redundant;
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,13 +7,6 @@ use crate::common::spec::tag::omission::{can_omit_as_before, can_omit_as_last_no
|
||||||
use crate::minify::attr::{minify_attr, AttrMinified};
|
use crate::minify::attr::{minify_attr, AttrMinified};
|
||||||
use crate::minify::content::minify_content;
|
use crate::minify::content::minify_content;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
|
||||||
enum LastAttr {
|
|
||||||
NoValue,
|
|
||||||
Quoted,
|
|
||||||
Unquoted,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn minify_element(
|
pub fn minify_element(
|
||||||
cfg: &Cfg,
|
cfg: &Cfg,
|
||||||
out: &mut Vec<u8>,
|
out: &mut Vec<u8>,
|
||||||
|
@ -30,49 +23,63 @@ pub fn minify_element(
|
||||||
closing_tag: ElementClosingTag,
|
closing_tag: ElementClosingTag,
|
||||||
children: Vec<NodeData>,
|
children: Vec<NodeData>,
|
||||||
) {
|
) {
|
||||||
|
// Output quoted attributes, followed by unquoted, to optimise space omission between attributes.
|
||||||
|
let mut quoted = Vec::new();
|
||||||
|
let mut unquoted = Vec::new();
|
||||||
|
|
||||||
|
for (name, value) in attributes {
|
||||||
|
match minify_attr(cfg, ns, tag_name, &name, value) {
|
||||||
|
AttrMinified::Redundant => {}
|
||||||
|
a @ AttrMinified::NoValue => unquoted.push((name, a)),
|
||||||
|
AttrMinified::Value(v) => {
|
||||||
|
debug_assert!(v.len() > 0);
|
||||||
|
if v.quoted() {
|
||||||
|
quoted.push((name, v));
|
||||||
|
} else {
|
||||||
|
unquoted.push((name, AttrMinified::Value(v)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attributes list could become empty after minification, so check opening tag omission eligibility after attributes minification.
|
||||||
let can_omit_opening_tag = (tag_name == b"html" || tag_name == b"head")
|
let can_omit_opening_tag = (tag_name == b"html" || tag_name == b"head")
|
||||||
&& attributes.is_empty()
|
&& quoted.len() + unquoted.len() == 0
|
||||||
&& !cfg.keep_html_and_head_opening_tags;
|
&& !cfg.keep_html_and_head_opening_tags;
|
||||||
let can_omit_closing_tag = !cfg.keep_closing_tags
|
let can_omit_closing_tag = !cfg.keep_closing_tags
|
||||||
&& (can_omit_as_before(tag_name, next_sibling_as_element_tag_name)
|
&& (can_omit_as_before(tag_name, next_sibling_as_element_tag_name)
|
||||||
|| (is_last_child_text_or_element_node && can_omit_as_last_node(parent, tag_name)));
|
|| (is_last_child_text_or_element_node && can_omit_as_last_node(parent, tag_name)));
|
||||||
|
|
||||||
// TODO Attributes list could become empty after minification, making opening tag eligible for omission again.
|
|
||||||
if !can_omit_opening_tag {
|
if !can_omit_opening_tag {
|
||||||
out.push(b'<');
|
out.push(b'<');
|
||||||
out.extend_from_slice(tag_name);
|
out.extend_from_slice(tag_name);
|
||||||
let mut last_attr = LastAttr::NoValue;
|
|
||||||
// TODO Further optimisation: order attrs based on optimal spacing strategy, given that spaces can be omitted after quoted attrs, and maybe after the tag name?
|
for (i, (name, value)) in quoted.iter().enumerate() {
|
||||||
let mut attrs_sorted = attributes.into_iter().collect::<Vec<_>>();
|
if i == 0 || cfg.keep_spaces_between_attributes {
|
||||||
attrs_sorted.sort_unstable_by(|a, b| a.0.cmp(&b.0));
|
|
||||||
for (name, value) in attrs_sorted {
|
|
||||||
let min = minify_attr(cfg, ns, tag_name, &name, value);
|
|
||||||
if let AttrMinified::Redundant = min {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if cfg.keep_spaces_between_attributes || last_attr != LastAttr::Quoted {
|
|
||||||
out.push(b' ');
|
out.push(b' ');
|
||||||
};
|
};
|
||||||
out.extend_from_slice(&name);
|
out.extend_from_slice(&name);
|
||||||
match min {
|
out.push(b'=');
|
||||||
AttrMinified::NoValue => {
|
debug_assert!(value.quoted());
|
||||||
last_attr = LastAttr::NoValue;
|
value.out(out);
|
||||||
}
|
}
|
||||||
AttrMinified::Value(v) => {
|
for (i, (name, value)) in unquoted.iter().enumerate() {
|
||||||
debug_assert!(v.len() > 0);
|
// Write a space between unquoted attributes,
|
||||||
out.push(b'=');
|
// and after the tag name if it wasn't written already during `quoted` processing.
|
||||||
v.out(out);
|
if i > 0 || (i == 0 && quoted.len() == 0) {
|
||||||
last_attr = if v.quoted() {
|
out.push(b' ');
|
||||||
LastAttr::Quoted
|
};
|
||||||
} else {
|
out.extend_from_slice(&name);
|
||||||
LastAttr::Unquoted
|
if let AttrMinified::Value(v) = value {
|
||||||
};
|
out.push(b'=');
|
||||||
}
|
v.out(out);
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if closing_tag == ElementClosingTag::SelfClosing {
|
if closing_tag == ElementClosingTag::SelfClosing {
|
||||||
if last_attr == LastAttr::Unquoted {
|
// Write a space after the tag name if there are no attributes,
|
||||||
|
// or the last attribute is unquoted.
|
||||||
|
if unquoted.len() > 0 || unquoted.len() + quoted.len() == 0 {
|
||||||
out.push(b' ');
|
out.push(b' ');
|
||||||
};
|
};
|
||||||
out.push(b'/');
|
out.push(b'/');
|
||||||
|
|
|
@ -22,6 +22,6 @@ js-esbuild = ["crossbeam", "esbuild-rs"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aho-corasick = "0.7"
|
aho-corasick = "0.7"
|
||||||
crossbeam = { version = "0.7", optional = true }
|
crossbeam = { version = "0.7", optional = true }
|
||||||
esbuild-rs = { version = "0.12.18", optional = true }
|
esbuild-rs = { version = "0.12.19", optional = true }
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
memchr = "2"
|
memchr = "2"
|
||||||
|
|
Loading…
Reference in New Issue