#!/usr/bin/env node /** * Benchmark AVIF encode options (+ JPEG/WebP reference) on a real fixture. * Usage: node scripts/bench-avif-encode.mjs [path-to-avif] */ import { readFile } from 'node:fs/promises'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import decodeAvif, { init as initAvifDecode } from '@jsquash/avif/decode.js'; import encodeAvif, { init as initAvifEncode } from '@jsquash/avif/encode.js'; import encodeJpeg, { init as initJpegEncode } from '@jsquash/jpeg/encode.js'; import encodeWebp, { init as initWebpEncode } from '@jsquash/webp/encode.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = resolve(__dirname, '..'); const RUNS = 3; const fixturePath = resolve( process.argv[2] ?? resolve(root, 'tmp/bench-source.avif'), ); async function compileWasm(relPath) { const bytes = await readFile(resolve(root, relPath)); return WebAssembly.compile(bytes); } function median(values) { const sorted = [...values].sort((a, b) => a - b); return sorted[Math.floor(sorted.length / 2)]; } async function timeEncode(label, fn) { const times = []; let bytes = 0; for (let i = 0; i < RUNS; i += 1) { const start = performance.now(); const buf = await fn(); times.push(performance.now() - start); bytes = buf.byteLength ?? buf.length; } return { label, encodeMs: Math.round(median(times)), outputBytes: bytes, }; } console.log('init wasm…'); await initAvifDecode(await compileWasm('node_modules/@jsquash/avif/codec/dec/avif_dec.wasm')); await initAvifEncode(await compileWasm('node_modules/@jsquash/avif/codec/enc/avif_enc.wasm')); await initJpegEncode(await compileWasm('node_modules/@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm')); await initWebpEncode(await compileWasm('node_modules/@jsquash/webp/codec/enc/webp_enc.wasm')); const raw = await readFile(fixturePath); console.log(`fixture: ${fixturePath} (${raw.byteLength} bytes)`); const decodeStarted = performance.now(); const imageData = await decodeAvif(raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength)); const decodeMs = Math.round(performance.now() - decodeStarted); console.log(`decode: ${decodeMs}ms → ${imageData.width}×${imageData.height}`); // Warmup await encodeAvif(imageData, { quality: 80, speed: 10 }); const variants = [ { label: 'avif q80/s6 (baseline)', fn: () => encodeAvif(imageData, { quality: 80, speed: 6 }) }, { label: 'avif q80/s8', fn: () => encodeAvif(imageData, { quality: 80, speed: 8 }) }, { label: 'avif q80/s10', fn: () => encodeAvif(imageData, { quality: 80, speed: 10 }) }, { label: 'avif q70/s8', fn: () => encodeAvif(imageData, { quality: 70, speed: 8 }) }, { label: 'avif q70/s10', fn: () => encodeAvif(imageData, { quality: 70, speed: 10 }) }, { label: 'jpeg q80 (ref)', fn: () => encodeJpeg(imageData, { quality: 80 }) }, { label: 'webp q80 (ref)', fn: () => encodeWebp(imageData, { quality: 80 }) }, ]; const rows = []; for (const variant of variants) { process.stdout.write(`… ${variant.label}\n`); rows.push(await timeEncode(variant.label, variant.fn)); } const baseline = rows.find((row) => row.label.includes('baseline')); console.log('\nresults (median of 3 runs):'); console.log( 'label'.padEnd(28), 'encodeMs'.padStart(10), 'bytes'.padStart(10), 'vsBaseMs'.padStart(10), 'vsBaseSz'.padStart(10), ); for (const row of rows) { const vsMs = baseline ? `${(row.encodeMs / baseline.encodeMs).toFixed(2)}x` : '-'; const vsSz = baseline ? `${(row.outputBytes / baseline.outputBytes).toFixed(2)}x` : '-'; console.log( row.label.padEnd(28), String(row.encodeMs).padStart(10), String(row.outputBytes).padStart(10), vsMs.padStart(10), vsSz.padStart(10), ); } console.log(JSON.stringify({ fixturePath, decodeMs, size: `${imageData.width}×${imageData.height}`, rows, }, null, 2));