Mike Allanson

#Minify JavaScript with esbuild

esbuild's website says it's:

An extremely fast JavaScript bundler

I want to minify some code without worrying about bundling. esbuild can do that too.

Some recent minification benchmarks mark it significantly faster than most other bundlers, and within a few percent on output filesize.

Sounds good to me!

Here's how I'm using it:

import esbuild from "esbuild";
import fs from "fs/promises";
async function* getFiles(path = `./`) {
const entries = await fs.readdir(path, { withFileTypes: true });
for (let file of entries) {
if (file.isDirectory()) {
yield* getFiles(`${path}${file.name}/`);
} else {
yield path + file.name;
}
}
}
async function main(dir) {
const options = {
format: "esm",
minify: true,
target: "es2020",
};
try {
for await (const filePath of getFiles(dir)) {
if (filePath.endsWith(".js")) {
const fileContents = await fs.readFile(filePath, "utf-8");
const transformed = await service.transform(fileContents, options);
await fs.writeFile(filePath, transformed.code);
}
}
} finally {
service.stop();
}
}
await main("public/");

This uses esbuild's Service API, to start a long running esbuild server, then pushes files through to esbuild to be minified.

While reading the Service API docs again, I realised that we don't have to minify each file one-by-one. We can shove all the files through to esbuild and let it process them in parallel. Here's a second version that does that. It creates an array of promises, with each promise responsible for minifying one file.

import esbuild from "esbuild";
import fs from "fs/promises";
async function* getFiles(path = `./`) {
const entries = await fs.readdir(path, { withFileTypes: true });
for (let file of entries) {
if (file.isDirectory()) {
yield* getFiles(`${path}${file.name}/`);
} else {
yield path + file.name;
}
}
}
const minifyOptions = {
format: "esm",
minify: true,
target: "es2020",
};
async function minifyFile(filePath, minifier) {
const fileContents = await fs.readFile(filePath, "utf-8");
const transformed = await minifier.transform(fileContents, minifyOptions);
return await fs.writeFile(filePath, transformed.code);
}
async function main(dir) {
let minifier = await esbuild.startService();
let promises = [];
try {
for await (const filePath of getFiles(dir)) {
if (filePath.endsWith(".js")) {
promises.push(minifyFile(filePath, minifier));
}
}
await Promise.all(promises);
} finally {
minifier.stop();
}
}

On my machine this minified ten files in ~22 milliseconds, vs ~35 milliseconds for the first version.

I'd have to run this script millions of times to see any benefit from this optimisation. But at least I can confidently state my official conclusion: esbuild is pretty neat.

#Minifying other file types

esbuild has bundling support for CSS. I haven't (yet) tested whether I can use it to minify CSS. Maybe it will handle JSON too?