61 lines
2.0 KiB
JavaScript
61 lines
2.0 KiB
JavaScript
|
|
// Precompress built SPA assets so the Rust web layer can serve them with
|
||
|
|
// `ServeDir::precompressed_br()/precompressed_gzip()` instead of re-running
|
||
|
|
// Brotli over the same immutable bundle on every request (the tower-http
|
||
|
|
// CompressionLayer stays as the on-the-fly fallback for anything without a
|
||
|
|
// sibling). Runs as the `build` script's final step; uses only node:zlib —
|
||
|
|
// no dependencies. See benches/STATIC-PRECOMPRESSED.md for the measured win.
|
||
|
|
import { promises as fs } from 'node:fs';
|
||
|
|
import path from 'node:path';
|
||
|
|
import zlib from 'node:zlib';
|
||
|
|
|
||
|
|
const OUT_DIR = process.argv[2] ?? '../static-dist';
|
||
|
|
// Compressible text assets; media formats are already compressed.
|
||
|
|
const EXTENSIONS = new Set([
|
||
|
|
'.js',
|
||
|
|
'.mjs',
|
||
|
|
'.css',
|
||
|
|
'.html',
|
||
|
|
'.svg',
|
||
|
|
'.json',
|
||
|
|
'.txt',
|
||
|
|
'.xml',
|
||
|
|
'.map',
|
||
|
|
'.webmanifest'
|
||
|
|
]);
|
||
|
|
// Below this size the encoding overhead outweighs the transfer win
|
||
|
|
// (mirrors the server's SizeAbove(256) predicate).
|
||
|
|
const MIN_BYTES = 256;
|
||
|
|
|
||
|
|
async function* walk(dir) {
|
||
|
|
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
||
|
|
const p = path.join(dir, entry.name);
|
||
|
|
if (entry.isDirectory()) yield* walk(p);
|
||
|
|
else yield p;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let files = 0;
|
||
|
|
let inBytes = 0;
|
||
|
|
let brBytes = 0;
|
||
|
|
for await (const file of walk(OUT_DIR)) {
|
||
|
|
if (!EXTENSIONS.has(path.extname(file))) continue;
|
||
|
|
const data = await fs.readFile(file);
|
||
|
|
if (data.length < MIN_BYTES) continue;
|
||
|
|
const br = zlib.brotliCompressSync(data, {
|
||
|
|
params: {
|
||
|
|
[zlib.constants.BROTLI_PARAM_QUALITY]: 11,
|
||
|
|
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: data.length
|
||
|
|
}
|
||
|
|
});
|
||
|
|
const gz = zlib.gzipSync(data, { level: 9 });
|
||
|
|
// Only keep siblings that actually shrink the asset.
|
||
|
|
if (br.length < data.length) await fs.writeFile(`${file}.br`, br);
|
||
|
|
if (gz.length < data.length) await fs.writeFile(`${file}.gz`, gz);
|
||
|
|
files += 1;
|
||
|
|
inBytes += data.length;
|
||
|
|
brBytes += Math.min(br.length, data.length);
|
||
|
|
}
|
||
|
|
console.log(
|
||
|
|
`precompress: ${files} assets, ${(inBytes / 1024).toFixed(0)} KiB → ${(brBytes / 1024).toFixed(0)} KiB brotli (${inBytes ? ((1 - brBytes / inBytes) * 100).toFixed(0) : 0}% smaller)`
|
||
|
|
);
|