From 94022402b4ac130d6f3b4b7805949054154ca97a Mon Sep 17 00:00:00 2001 From: Vlad Sirenko Date: Wed, 19 Jun 2024 04:00:19 -0700 Subject: [PATCH] feature: add benchmark (#27) --- .gitignore | 1 + .npmignore | 1 + .nycrc.json | 1 + README.md | 12 +-- benchmark/.gitignore | 7 ++ benchmark/.putout.json | 7 ++ benchmark/README.md | 55 ++++++++++ benchmark/cli/minify.js | 6 ++ benchmark/index.js | 191 +++++++++++++++++++++++++++++++++++ benchmark/package.json | 20 ++++ benchmark/source/example1.js | 28 +++++ 11 files changed, 319 insertions(+), 10 deletions(-) create mode 100644 benchmark/.gitignore create mode 100644 benchmark/.putout.json create mode 100644 benchmark/README.md create mode 100644 benchmark/cli/minify.js create mode 100644 benchmark/index.js create mode 100644 benchmark/package.json create mode 100644 benchmark/source/example1.js diff --git a/.gitignore b/.gitignore index 1cb47a1..5311eff 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ yarn-error.log coverage .idea bundle +.DS_Store diff --git a/.npmignore b/.npmignore index 12a00d3..93924c1 100644 --- a/.npmignore +++ b/.npmignore @@ -10,3 +10,4 @@ rules rollup.config.js lib/*.js *.config.* +benchmark diff --git a/.nycrc.json b/.nycrc.json index dc72b6e..f8eadf4 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -12,6 +12,7 @@ "**/bundle", "**/stub", "rollup.config.js", + "benchmark", "**/*.config.*" ], "branches": 100, diff --git a/README.md b/README.md index dbbb6b4..9074459 100644 --- a/README.md +++ b/README.md @@ -98,17 +98,9 @@ minify(source, { }); ``` -### How it's compared to [Terser](https://github.com/terser/terser)? +### How it's compared to X(your benchmark)? -For [such code](https://github.com/coderaiser/minify/issues/96#issuecomment-1546605157): - -- 🔥 `@putout/minify`: `475B` -- ❌ `terser`: `482B` - -`react.js`: - -- 🔥 `@putout/minify`: `16309B` -- ❌ `terser`: `16346B` +[Benchmark](benchmark) ## License diff --git a/benchmark/.gitignore b/benchmark/.gitignore new file mode 100644 index 0000000..389046e --- /dev/null +++ b/benchmark/.gitignore @@ -0,0 +1,7 @@ +source/* +!source/example* +result +.idea +*.swp +yarn-error.log +coverage diff --git a/benchmark/.putout.json b/benchmark/.putout.json new file mode 100644 index 0000000..ca41ac3 --- /dev/null +++ b/benchmark/.putout.json @@ -0,0 +1,7 @@ +{ + "rules": { + "remove-unused-variables": "off", + "remove-console": "off", + "remove-useless-arguments/arguments": "off" + } +} \ No newline at end of file diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 0000000..8ff2332 --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,55 @@ +# Benchmark + +This is a performance benchmark of the following bundlers: + +- Bun +- esbuild +- Terser +- @putoutjs/minify + +To run the benchmark: + +- Need to install bun globally (https://bun.sh/docs/installation) + +```sh +$ npm install +$ node index.js +``` + +## Results + +The `real` results, as run on a 13-inch M1 Macbook air: + +Time in ms + +``` +┌──────────┬─────┬─────────┬────────┬─────────────────┐ +│ (index) │ bun │ esbuild │ terser │ putoutjs_minify │ +├──────────┼─────┼─────────┼────────┼─────────────────┤ +│ react │ 58 │ 50 │ 230 │ 2509 │ +│ solidjs │ 21 │ 16 │ 270 │ 12735 │ +│ treejs │ 74 │ 100 │ 1787 │ │ +│ lodash │ 21 │ 27 │ 603 │ 108901 │ +│ vue │ 60 │ 62 │ 1455 │ │ +│ angular │ 32 │ 41 │ 1320 │ │ +│ jquery │ 18 │ 23 │ 548 │ 80481 │ +│ example1 │ 42 │ 35 │ 108 │ 330 │ +└──────────┴─────┴─────────┴────────┴─────────────────┘ +``` + +Size in bytes + +``` +┌──────────┬───────────────────┬───────────────────┬───────────────────┬──────────────────┬──────────────────┐ +│ (index) │ original │ bun │ esbuild │ terser │ putoutjs_minify │ +├──────────┼───────────────────┼───────────────────┼───────────────────┼──────────────────┼──────────────────┤ +│ react │ '10751 (100.0%)' │ '10553 (98.2%)' │ '10644 (99.0%)' │ '10391 (96.7%)' │ '10052 (93.5%)' │ +│ solidjs │ '28537 (100.0%)' │ '21888 (76.7%)' │ '20553 (72.0%)' │ '28202 (98.8%)' │ '21886 (76.7%)' │ +│ treejs │ '677935 (100.0%)' │ '680368 (100.4%)' │ '657528 (97.0%)' │ '677573 (99.9%)' │ │ +│ lodash │ '73015 (100.0%)' │ '72551 (99.4%)' │ '72189 (98.9%)' │ '70751 (96.9%)' │ '73688 (100.9%)' │ +│ vue │ '196075 (100.0%)' │ '196443 (100.2%)' │ '193091 (98.5%)' │ '195200 (99.6%)' │ │ +│ angular │ '177368 (100.0%)' │ '178187 (100.5%)' │ '177834 (100.3%)' │ '176388 (99.4%)' │ │ +│ jquery │ '87533 (100.0%)' │ '87451 (99.9%)' │ '87119 (99.5%)' │ '86958 (99.3%)' │ '85889 (98.1%)' │ +│ example1 │ '853 (100.0%)' │ '494 (57.9%)' │ '490 (57.4%)' │ '481 (56.4%)' │ '482 (56.5%)' │ +└──────────┴───────────────────┴───────────────────┴───────────────────┴──────────────────┴──────────────────┘ +``` diff --git a/benchmark/cli/minify.js b/benchmark/cli/minify.js new file mode 100644 index 0000000..d94e2f6 --- /dev/null +++ b/benchmark/cli/minify.js @@ -0,0 +1,6 @@ +import {readFileSync, writeFileSync} from 'node:fs'; +import process from 'node:process'; +import {minify} from '@putout/minify'; + +const body = readFileSync(process.argv[2], 'utf8'); +writeFileSync(process.argv[3], minify(body)); diff --git a/benchmark/index.js b/benchmark/index.js new file mode 100644 index 0000000..478f036 --- /dev/null +++ b/benchmark/index.js @@ -0,0 +1,191 @@ +import {get} from 'node:https'; +import {execSync} from 'node:child_process'; +import { + statSync, + writeFileSync, + createWriteStream, + rmSync, +} from 'node:fs'; + +const listFiles = { + react: { + url: 'https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js', + }, + solidjs: { + url: 'https://cdn.jsdelivr.net/npm/solid-js@1.8.17/dist/solid.min.js', + }, + treejs: { + url: 'https://cdn.jsdelivr.net/npm/three@0.165.0/build/three.module.min.js', + }, + lodash: { + url: 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js', + }, + vue: { + url: 'https://cdn.jsdelivr.net/npm/vue@3.4.29/dist/vue.global.min.js', + }, + angular: { + url: 'https://cdn.jsdelivr.net/npm/angular@1.8.3/angular.min.js', + }, + jquery: { + url: 'https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js', + }, + example1: { + file: './source/example1.js', + }, +}; + +const compressors = { + bun: 'bun build --minify {dist} --outfile {out}', + esbuild: './node_modules/.bin/esbuild {dist} --bundle --minify --outfile={out}', + terser: './node_modules/.bin/terser {dist} --compress --mangle --comments false -o {out}', + putoutjs_minify: 'node ./cli/minify.js {dist} {out}', +}; + +const debug = () => {}; + +// debug = console.log; +function fileExists(path) { + try { + const s = statSync(path); + debug(`File ${path} exists ${s.size} bytes`); + + return s.size > 0; + } catch(e) { + debug(`File ${path} does not exist`); + return false; + } +} + +function downloadFile(url, dest) { + if (fileExists(dest)) { + debug(`File ${dest} already exists`); + return; + } + + return new Promise((resolve, reject) => { + debug(`Downloading ${url} to ${dest}`); + get(url, (res) => { + const file = createWriteStream(dest, ''); + res.pipe(file); + res.on('end', () => { + debug(`Downloaded ${url} to ${dest}`); + resolve(); + }); + }); + }); +} + +function fileFromUrl(url) { + return `./source/${url}.js`; +} + +async function download() { + for (const [key, value] of Object.entries(listFiles)) { + if (!value.url) + continue; + + const {url} = value; + const dest = fileFromUrl(key); + + await downloadFile(url, dest); + } +} + +function getPathFromFile(file) { + return listFiles[file].file || fileFromUrl(file); +} + +function removeFile(filePath) { + rmSync(filePath, { + force: true, + }); +} + +function compareTask(file) { + const dist = getPathFromFile(file); + const result = {}; + + for (const [key, value] of Object.entries(compressors)) { + const out = `./result/${file}-${key}.js`; + removeFile(out); + + const cmd = value + .replace('{dist}', dist) + .replace('{out}', out); + + debug(`Running: ${cmd}`); + const endTime = 0; + + try { + const startTime = Date.now(); + + execSync(cmd, { + stdio: 'pipe', + encoding: 'utf8', + }); + + const endTime = Date.now() - startTime; + } catch(e) { + console.error(`Error running ${key}: ${e.message} ${e.stderr}`); + continue; + } + + const fileSize = statSync(out).size; + + result[key] = { + time: endTime, + size: fileSize, + }; + console.log(`File: ${file} Compressor: ${key} Time: ${endTime}ms Size: ${fileSize} bytes`); + } + + return result; +} + +function convertMap(fn, obj) { + return Object.fromEntries(Object + .entries(obj) + .map(([k, v]) => [k, fn(v, k, obj)]) + .filter(([k, v]) => v !== undefined)); +} + +function convertTable(obj, key) { + return convertMap(convertMap.bind(null, (v) => v[key]), obj); +} + +function compare() { + const result = {}; + + for (const key of Object.keys(listFiles)) { + result[key] = compareTask(key); + const originalSize = statSync(getPathFromFile(key)).size; + + result[key] = { + original: { + size: originalSize, + }, + ...result[key], + }; + } + + return result; +} + +async function main() { + await download(); + const result = compare(); + + debug(result); + const resultTime = convertTable(result, 'time'); + + debug(resultTime); + console.log('Time in ms'); + console.table(resultTime); + const resultSize = convertMap(convertMap.bind(null, (v, k, obj) => `${v} (${(100 * v / obj.original).toFixed(1)}%)`), convertTable(result, 'size')); + + debug(resultSize); + console.log('Size in bytes'); + console.table(resultSize); +} + +main(); diff --git a/benchmark/package.json b/benchmark/package.json new file mode 100644 index 0000000..35b5d4b --- /dev/null +++ b/benchmark/package.json @@ -0,0 +1,20 @@ +{ + "name": "@putout/minify-benchmark", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "homepage": "https://github.com/putoutjs/minify/tree/master/benchmark#readme", + "license": "MIT", + "repository": { + "type": "git", + "url": "git://github.com/putoutjs/minify.git" + }, + "dependencies": { + "@putout/minify": "^4.1.0", + "esbuild": "^0.21.5", + "terser": "^5.31.1" + } +} diff --git a/benchmark/source/example1.js b/benchmark/source/example1.js new file mode 100644 index 0000000..40b33f5 --- /dev/null +++ b/benchmark/source/example1.js @@ -0,0 +1,28 @@ +/* global jQuery */ +const isUndefined = (a) => typeof a === 'undefined'; + +jQuery(($) => { + let dp; + + if (!isUndefined($.datepicker)) { + $('#ep_ipo_date').datepicker({ + dateFormat: 'yy-mm-dd', + changeYear: true, + changeMonth: true, + }); + $('#ep_ipo_date_dp').click(function() { + if ($('#ui-datepicker-div').is(':visible') && dp === $(this).attr('id')) { + $('#ep_ipo_date').datepicker('hide'); + const dp = null; + } else { + $('#ep_ipo_date').datepicker('show'); + const dp = $(this).attr('id'); + } + }); + + $('#ep_ipo_date').focusout(function() { + if (!/^(?:\d{2}\/){2}\d{4}$/.test($(this).val()) && $(this).val().length) + $(this).val(''); + }); + } +});