From 4ff2e14b045ca5e29f2f28b5152e88e2fcac9fb2 Mon Sep 17 00:00:00 2001 From: Wilson Lin Date: Fri, 24 Jul 2020 18:24:28 +1000 Subject: [PATCH] Use esbuild for minifiers in bench --- bench/README.md | 1 + bench/minifiers.js | 131 +++++++++++++++++++++++++++++++-------------- bench/package.json | 4 +- bench/sizes.js | 32 +++++------ bench/speeds.js | 7 ++- 5 files changed, 116 insertions(+), 59 deletions(-) diff --git a/bench/README.md b/bench/README.md index 38123b1..70f57af 100644 --- a/bench/README.md +++ b/bench/README.md @@ -42,6 +42,7 @@ Since speed depends on the input, speed charts show performance relative to the The settings used for each minifier can be found in [minifiers.js](./minifiers.js). Some settings to note: - CSS minification is disabled for all, as minify-html currently does not support CSS minification (coming soon). +- To increase fairness, all minifiers use esbuild for JS minification, and do so asynchronously and in parallel, similar to how minify-html works. - `conservativeCollapse` is enabled for html-minifier as otherwise some whitespace would be unsafely removed with side affects. minify-html can safely remove whitespace with context if configured properly. ## Running diff --git a/bench/minifiers.js b/bench/minifiers.js index 3d496e7..7bde60a 100644 --- a/bench/minifiers.js +++ b/bench/minifiers.js @@ -1,43 +1,94 @@ -const htmlMinifier = require("html-minifier"); -const minifyHtml = require("@minify-html/js"); -const minimize = require("minimize"); -const terser = require('terser'); +const esbuild = require('esbuild'); +const htmlMinifier = require('html-minifier'); +const minifyHtml = require('@minify-html/js'); +const minimize = require('minimize'); + +const minifyHtmlCfg = minifyHtml.createConfiguration({minifyJs: true}); + +const jsMime = new Set([ + undefined, + 'application/ecmascript', + 'application/javascript', + 'application/x-ecmascript', + 'application/x-javascript', + 'text/ecmascript', + 'text/javascript', + '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', +]); + +class EsbuildAsync { + constructor () { + this.promises = []; + } + + queue (code) { + const id = this.promises.push(esbuild.transform(code, { + minify: true, + minifyWhitespace: true, + minifyIdentifiers: true, + minifySyntax: true, + })) - 1; + return `_____ESBUILD_ASYNC_PLACEHOLDER_${id}_____`; + } + + async finalise (html) { + const jsTransformResults = await Promise.all(this.promises); + return html.replace(/_____ESBUILD_ASYNC_PLACEHOLDER_([0-9]+)_____/g, (_, id) => jsTransformResults[id].js.replace(/<\/script/g, '<\\/script')); + } +} module.exports = { - 'minify-html-nodejs': (_, buffer) => minifyHtml.minifyInPlace(Buffer.from(buffer), {minifyJs: true}), - 'html-minifier': content => htmlMinifier.minify(content, { - collapseBooleanAttributes: true, - collapseInlineTagWhitespace: true, - collapseWhitespace: true, - // minify-html can do context-aware whitespace removal, which is safe when configured correctly to match how whitespace is used in the document. - // html-minifier cannot, so whitespace must be collapsed conservatively. - // Alternatively, minify-html can also be made to remove whitespace regardless of context. - conservativeCollapse: true, - customEventAttributes: [], - decodeEntities: true, - ignoreCustomComments: [], - ignoreCustomFragments: [/<\?[\s\S]*?\?>/], - minifyJS: true, - processConditionalComments: true, - removeAttributeQuotes: true, - removeComments: true, - removeEmptyAttributes: true, - removeOptionalTags: true, - removeRedundantAttributes: true, - removeScriptTypeAttributes: true, - removeStyleLinkTypeAttributes: true, - removeTagWhitespace: true, - useShortDoctype: true, - }), - 'minimize': content => new minimize({ - plugins: [{ - id: 'terser', - element: (node, next) => { - if (node.type === 'text' && node.parent && node.parent.type === 'script') { - node.data = terser.minify(node.data).code || node.data; - } - next(); - }, - }] - }).parse(content), + 'minify-html-nodejs': (_, buffer) => minifyHtml.minifyInPlace(Buffer.from(buffer), minifyHtmlCfg), + 'html-minifier': async (content) => { + const js = new EsbuildAsync(); + const res = htmlMinifier.minify(content, { + collapseBooleanAttributes: true, + collapseInlineTagWhitespace: true, + collapseWhitespace: true, + // minify-html can do context-aware whitespace removal, which is safe when configured correctly to match how whitespace is used in the document. + // html-minifier cannot, so whitespace must be collapsed conservatively. + // Alternatively, minify-html can also be made to remove whitespace regardless of context. + conservativeCollapse: true, + customEventAttributes: [], + decodeEntities: true, + ignoreCustomComments: [], + ignoreCustomFragments: [/<\?[\s\S]*?\?>/], + minifyJS: code => js.queue(code), + processConditionalComments: true, + removeAttributeQuotes: true, + removeComments: true, + removeEmptyAttributes: true, + removeOptionalTags: true, + removeRedundantAttributes: true, + removeScriptTypeAttributes: true, + removeStyleLinkTypeAttributes: true, + removeTagWhitespace: true, + useShortDoctype: true, + }); + return js.finalise(res); + }, + 'minimize': async (content) => { + const js = new EsbuildAsync(); + const res = new minimize({ + plugins: [{ + id: 'esbuild', + element: (node, next) => { + if (node.type === 'text' && node.parent && node.parent.type === 'script' && jsMime.has(node.parent.attribs.type)) { + node.data = js.queue(node.data); + } + next(); + }, + }], + }).parse(content); + return js.finalise(res); + }, }; diff --git a/bench/package.json b/bench/package.json index 2541ffb..c3eda9d 100644 --- a/bench/package.json +++ b/bench/package.json @@ -5,13 +5,13 @@ "benchmark": "2.1.4", "chart.js": "^2.9.3", "chartjs-node": "^1.7.1", + "esbuild": "^0.6.5", "html-minifier": "4.0.0", "minimize": "2.2.0", "minimist": "^1.2.0", "mkdirp": "^0.5.1", "request": "^2.88.0", - "request-promise-native": "^1.0.8", - "terser": "^4.8.0" + "request-promise-native": "^1.0.8" }, "engines": { "node": "10.x" diff --git a/bench/sizes.js b/bench/sizes.js index fdeef42..a1817cd 100644 --- a/bench/sizes.js +++ b/bench/sizes.js @@ -22,20 +22,22 @@ const setSize = (program, test, result) => { }; }; -for (const t of tests) { - for (const m of Object.keys(minifiers)) { - try { - const min = minifiers[m](t.contentAsString, t.contentAsBuffer); - // If `min` is a Buffer, convert to string (interpret as UTF-8) to get canonical length. - setSize(m, t.name, min.toString().length); - const minPath = path.join(__dirname, 'min', m, `${t.name}.html`); - mkdirp.sync(path.dirname(minPath)); - fs.writeFileSync(minPath, min); - } catch (err) { - console.error(`Failed to run ${m} on test ${t.name}:`); - console.error(err); - process.exit(1); +(async () => { + for (const t of tests) { + for (const m of Object.keys(minifiers)) { + try { + const min = await minifiers[m](t.contentAsString, t.contentAsBuffer); + // If `min` is a Buffer, convert to string (interpret as UTF-8) to get canonical length. + setSize(m, t.name, min.toString().length); + const minPath = path.join(__dirname, 'min', m, `${t.name}.html`); + mkdirp.sync(path.dirname(minPath)); + fs.writeFileSync(minPath, min); + } catch (err) { + console.error(`Failed to run ${m} on test ${t.name}:`); + console.error(err); + process.exit(1); + } } } -} -results.writeSizeResults(sizes); + results.writeSizeResults(sizes); +})(); diff --git a/bench/speeds.js b/bench/speeds.js index e38f4ad..b1bb587 100644 --- a/bench/speeds.js +++ b/bench/speeds.js @@ -44,8 +44,11 @@ const runTest = test => new Promise((resolve, reject) => { // Run JS libraries. const suite = new benchmark.Suite(); for (const m of Object.keys(minifiers)) { - suite.add(m, () => { - minifiers[m](test.contentAsString, test.contentAsBuffer); + suite.add(m, { + defer: true, + fn (deferred) { + Promise.resolve(minifiers[m](test.contentAsString, test.contentAsBuffer)).then(() => deferred.resolve()); + }, }); } suite