Skip to content

Performance and correctness audit ​

An independent pass over Optiq looking for outputs larger than the input, quality loss, lost metadata and WebAssembly overhead. Every number on this page comes from a script in bench/audit/ and its output file in bench/results/. Lossless semantics were not changed to save bytes.

Method ​

  • Fixtures: 17 edge cases generated deterministically from the benchmark corpus — a 12 MP phone JPEG with EXIF orientation 6, Display P3 profile, GPS and 4 KB XMP; sRGB+EXIF JPEG; P3 PNG photo; grayscale JPEG; an already-compressed q55 JPEG; palette PNG with alpha; 16-bit PNG; PNG with gAMA; 16×16 icon; lossy and lossless WebP; static and animated GIF; CMYK JPEG; AVIF; screenshot; transparent icon.
  • Variants: lossless, lossless + strip metadata, visually-lossless, balanced, balanced + strip, balanced + maxWidthOrHeight: 1024, balanced + targetFormat: "webp" — 119 runs.
  • Independent checks (no Optiq code involved): lossless claims → decode input and output with libvips and compare raw pixels (16-bit for 16-bit input); quality → ssim.js reference SSIM and PSNR; metadata → sharp.metadata() of the output (EXIF, GPS IFD, ICC bytes, XMP, orientation, frame count).
  • Profiling: wall time per pipeline phase through the engine's onTiming hook; codec microbenchmarks WASM vs native libvips with identical settings.

Reproduce: pnpm bench:corpus && pnpm build && node bench/audit/run.mjs && node bench/audit/codecs.mjs.

Findings ​

#FindingEvidenceAction
1visually-lossless returned the 794 KB original of a P3 photo PNG, while lossless mode found a 472 KB pixel-exact WebP. JPEG/WebP were capped at q95 (SSIM 0.9847 / 0.9898 < 0.99), AVIF is excluded for P3, and lossless candidates were never tried for photos.bench/results/audit-before-fixes.jsonQuality ceilings raised to JPEG 98 / WebP 99 / AVIF 95; lossless candidates are added when no re-encode passes and the input is stored losslessly. Now 794 KB → 171 KB (WebP, SSIM 0.9925; reference SSIM 0.998). Regression test added.
2Alpha errors were under-penalised. Composited SSIM gave an icon 0.994 while the alpha plane alone scored 0.972; AVIF alpha errors reached 128/255 at shape edges.bench/audit/alpha-metric.mjs, avif-alpha.mjsScore is now min(composited, alpha-plane SSIM). Costs bytes where alpha was being degraded (see below).
3Output larger than the input: lossless WebP screenshot + maxWidthOrHeight: 1024 → 24 KB in, 55 KB out. Resampling destroys flat colours (lossless WebP of the resized image: 159 KB).audit runCorrect under the caller's constraint; the result now carries an explicit warning.
4Before the audit, libvips AVIF beat ours on one transparent illustration (19.1 KB at SSIM 0.9938 vs 20.0 KB at 0.9910).bench/results/RESULTS.mdNot changed. Our alpha-quality boost costs those bytes: at q30–45 without it, the maximum alpha error outside transparent areas rose from 35–73 to 69–142 levels, and the icon showed alpha haze up to 65 in fully transparent areas (26 with the boost) (avif-alpha.mjs).
5libvips lossless WebP (effort 6) up to 3% smaller.codecs.mjsNot changed. Method 6 (quality 100) saves 0.9–2.8% but is 31–37× slower (0.95 s on a 1 MP screenshot, 6.1 s on a 0.4 MP photo).
64:2:0 vs 4:4:4 AVIF for graphicsavif-settings.mjsKept 4:4:4. Mixed per image: at SSIM 0.975, 4:2:0 was 23% smaller on the code screenshot but 4:4:4 was 13% smaller on the text page; at 0.99, 4:4:4 was 20% smaller on the text page and 4:2:0 could not reach 0.99 on the code screenshot at all. Summed over the 7 graphics images 4:4:4 is 4% smaller at 0.975. No single setting wins everywhere; trying both would add AVIF encodes, the most expensive step.

No metadata problems were found: EXIF and XMP were kept whenever preserveMetadata was on (AVIF is skipped rather than losing them), GPS was removed in every strip run, non-sRGB ICC profiles were never dropped, orientation was baked or kept as reported, animation was never flattened, and every lossless output was pixel-identical when decoded by libvips — including the 16-bit PNG at 16-bit depth.

Where the time goes ​

All 119 audit runs, by phase (after fixes):

PhaseShare
encode (WASM codecs)79.8%
SSIM (JS)9.7%
verify-decode (WASM)6.8%
resize, decode, classify, orientation, compare, probe, container3.7% combined

WASM vs native libvips, same settings, single thread, median:

ImageOperationWASM / JSNative
kodim23 (0.4 MP)decode jpeg2.2 ms1.3 ms1.65×
kodim23 (0.4 MP)encode jpeg q80 (mozjpeg)34.2 ms15.8 ms2.17×
kodim23 (0.4 MP)encode webp q80 m425.1 ms18.8 ms1.34×
kodim23 (0.4 MP)encode avif q50 speed6193.3 ms77.5 ms2.49× native effort 3 ≈ libaom speed 6
kodim23 (0.4 MP)encode webp lossless m4 q75172.0 ms–345944 B
kodim23 (0.4 MP)encode webp lossless m5 q75174.8 ms–345944 B
kodim23 (0.4 MP)encode webp lossless m6 q1006234.9 ms–342876 B
kodim23 (0.4 MP)encode png oxipng level 2311.4 ms–456173 B
kodim23 (0.4 MP)encode png oxipng level 3800.9 ms–443547 B
kodim23 (0.4 MP)ssim (JS)5.9 ms–
kodim23 (0.4 MP)exact compare (JS)0.0 ms–
kodim23 (0.4 MP)classify (JS)7.8 ms–
kodim23 (0.4 MP)RGBA copy (slice)0.1 ms–cost of one extra buffer copy
screenshot (1 MP)decode png6.2 ms3.6 ms1.71×
screenshot (1 MP)encode jpeg q80 (mozjpeg)88.4 ms41.9 ms2.11×
screenshot (1 MP)encode webp q80 m447.9 ms33.8 ms1.42×
screenshot (1 MP)encode avif q50 speed6298.6 ms215.7 ms1.38× native effort 3 ≈ libaom speed 6
screenshot (1 MP)encode webp lossless m4 q7530.8 ms–24372 B
screenshot (1 MP)encode webp lossless m5 q7542.4 ms–24372 B
screenshot (1 MP)encode webp lossless m6 q100949.8 ms–23678 B
screenshot (1 MP)encode png oxipng level 2313.4 ms–140516 B
screenshot (1 MP)encode png oxipng level 3966.0 ms–140516 B
screenshot (1 MP)ssim (JS)15.3 ms–
screenshot (1 MP)exact compare (JS)0.1 ms–
screenshot (1 MP)classify (JS)4.2 ms–
screenshot (1 MP)RGBA copy (slice)0.3 ms–cost of one extra buffer copy
phone (12 MP)decode jpeg61.9 ms57.6 ms1.07×
phone (12 MP)encode jpeg q80 (mozjpeg)1178.5 ms575.1 ms2.05×
phone (12 MP)encode webp q80 m4811.4 ms598.8 ms1.35×
phone (12 MP)ssim (JS)184.3 ms–
phone (12 MP)exact compare (JS)0.0 ms–
phone (12 MP)classify (JS)5.9 ms–
phone (12 MP)RGBA copy (slice)3.5 ms–cost of one extra buffer copy
phone (12 MP)tile mosaic (JS)2.3 ms–

Interpretation: the WASM codecs cost 1.1–2.5× native time (MozJPEG ~2.1×: the jSquash build has no SIMD). That is the price of running in the browser and cannot be removed without rebuilding the codecs. Buffer copies (3 ms at 12 MP), classification (≤ 8 ms) and module initialisation (40 ms in Node) are negligible. SSIM was the only overhead owned by Optiq that mattered: it cost ~25% of each candidate on small photos.

Optimisation applied: SSIM windows are 8 rows tall at stride 4, so every pixel was converted and accumulated twice. Partial sums are now kept per 4-row group. Results are numerically identical (test, |Δ| < 1e-10) and SSIM is 20–54% faster (12.7 → 5.9 ms at 0.4 MP, 19.7 → 15.3 ms at 1 MP, 231 → 184 ms at 12 MP). End-to-end time did not drop because fixes 1 and 2 add work (wider quality range, alpha-plane SSIM).

Before / after on the benchmark corpus (Node) ​

ToolImagesBytes before → afterChanged outputsTime before → after (sum)Min SSIM before → after
sc-balanced245.152 → 5.148 MB (-0.1%)268.2 → 70.0 s (3%)0.9752 → 0.9751
sc-balanced-nometa244.450 → 4.446 MB (-0.1%)288.4 → 89.0 s (1%)0.9753 → 0.9751
sc-visually-lossless246.383 → 6.407 MB (0.4%)10108.0 → 109.1 s (1%)0.9902 → 0.9902
sc-lossless2410.553 → 10.553 MB (0.0%)018.6 → 19.0 s (2%)1.0000 → 1.0000

Changed outputs:

ImageToolBytes before → afterSSIM before → afterFormat before → after
photo-kodim03.jpgsc-visually-lossless56948 → 53589 (-5.9%)0.9919 → 0.9908avif → avif
photo-kodim07.jpgsc-visually-lossless53730 → 56370 (4.9%)0.9903 → 0.9910avif → avif
photo-kodim15.jpgsc-visually-lossless72941 → 69356 (-4.9%)0.9917 → 0.9904avif → avif
photo-kodim20.jpgsc-visually-lossless48756 → 51201 (5.0%)0.9906 → 0.9913avif → avif
photo-kodim23.jpgsc-visually-lossless53956 → 58731 (8.8%)0.9902 → 0.9917avif → avif
photo-kodim19.pngsc-balanced74670 → 70744 (-5.3%)0.9772 → 0.9758avif → avif
photo-kodim19.pngsc-balanced-nometa74670 → 70744 (-5.3%)0.9772 → 0.9758avif → avif
photo-kodim19.pngsc-visually-lossless141333 → 157150 (11.2%)0.9913 → 0.9927avif → avif
transparent-illustration-1.pngsc-visually-lossless14366 → 15701 (9.3%)0.9902 → 0.9907avif → avif
transparent-illustration-2.pngsc-visually-lossless19979 → 21801 (9.1%)0.9910 → 0.9913avif → avif
transparent-illustration-3.pngsc-visually-lossless14286 → 16193 (13.3%)0.9910 → 0.9911avif → avif
transparent-icon.pngsc-balanced2035 → 2147 (5.5%)0.9953 → 0.9824avif → avif
transparent-icon.pngsc-balanced-nometa2035 → 2147 (5.5%)0.9953 → 0.9824avif → avif
transparent-icon.pngsc-visually-lossless2210 → 2836 (28.3%)0.9972 → 0.9930avif → avif

Trade-offs, stated plainly: visually-lossless grew 0.4% overall. Transparent images grew 5–28% because their alpha channel is no longer allowed to degrade (finding 2). Kodak photos moved ±5–9% in both directions because the wider quality range changes the bisection path; all still meet SSIM ≥ 0.99. lossless output is unchanged byte for byte.

Not covered ​

  • Firefox and Safari for the audit itself (the audit runs in Node; the browser test suite covers Chromium, Firefox and WebKit).
  • Peak memory in the browser (measured in Node only, see benchmarks).
  • Perceptual metrics beyond SSIM/PSNR (Butteraugli, SSIMULACRA2).