PureJsImage · JPEG XL
JPEG XL JavaScript library comparison
Dated public-API measurements across PureJsImage, jSquash, jxl-oxide-wasm and wasm-vips. Compare practical workflows and their limits.
These tools use the current repository build. Repository functionality may be newer than the npm release. Static lossy encoding is qualified; lossy animation remains Experimental.
Quick answer
PureJsImage now decodes all 16 pinned lossless inputs exactly, including the seven that previously failed. It has the smallest loaded toolkit among these tested APIs and exposes native sample and exact JPEG workflows. The WASM alternatives are faster on several workloads; most lossy quality comparisons remain unresolved under the finite sampling budget.
Measured 2026-10-01 on one Linux workstation. PureJsImage base revision 590bbf08968f0f12c32bb4aa5c84627ba5dbeab4 with local codec changes. Codec source SHA-256: 088ba519315f3498cd388eaf0c7bef92c0a15c6702107e04219ed486760332ca. These results measure a local repository build.
Additional fixture checks now cover multi-group global Palette and Squeeze streams, all 27 VarDCT transforms, grouped progressive alpha, and ordinary ICC and structured SDR conversion through 16-bit integer precision. Relative custom white points and primaries also convert to sRGB. Ordinary IEEE float input preserves source samples, converts supported ICC or structured SDR color to straight sRGB16, and supports explicit PQ/HLG/linear HDR conversion. Ordinary lossless output preserves binary32 samples and matching retained ICC profiles. Native Modular float and CMYK frames now compose before display conversion, including selected animation frames. Separate libjxl, LittleCMS and FFmpeg references pass in Node and three browsers. Further independent checks cover 24 mixed-alpha and wide-integer layouts, custom float fields, RGB LUT8/LUT16 XYZ profiles, relative custom HDR, integer ICC preservation, Float32 animation writing and selected HDR VarDCT frames. HLG and PQ forward writing support associated alpha and nondefault luminance targets. HDR VarDCT reference frames compose in linear color before the output transfer curve. Explicit lossy Float32 output uses Modular mantissa rounding with exact alpha. Independent native groups decode cropped bands. Remaining gaps include transformed implicit-palette prefix layouts, nonrelative custom color, wider ICC/CMYK profile families, wider integer VarDCT display, float VarDCT writing and progressive float-alpha or animation output. The speed and size comparison was rerun on the current source with the same pinned inputs and comparator versions. These additional capability checks cover cases beyond that shared comparison cohort. See the codec support checklist for the supported scope.
Correctness and API coverage
Chromium main set: 16 still inputs, with 13 lossy decode fixtures. Counts retain failures. Encode counts include multiple settings. “Incorrect” means the declared exact-sample or RGB8 tolerance contract failed, not a claim that the library is generally broken.
| Library | Operation | Verified / total | API absent | Unsupported | Incorrect | Execution failure | Unavailable / not tested |
|---|---|---|---|---|---|---|---|
| PureJsImage repository build | decode-lossless | 16 / 16 | 0 | 0 | 0 | 0 | 0 / 0 |
| PureJsImage repository build | decode-lossy | 13 / 13 | 0 | 0 | 0 | 0 | 0 / 0 |
| PureJsImage repository build | encode | 45 / 45 | 0 | 0 | 0 | 0 | 0 / 0 |
| PureJsImage repository build | roundtrip | 1 / 1 | 0 | 0 | 0 | 0 | 0 / 0 |
| @jsquash/jxl 1.3.0 | decode-lossless | 12 / 16 | 3 | 0 | 1 | 0 | 0 / 0 |
| @jsquash/jxl 1.3.0 | decode-lossy | 4 / 13 | 0 | 0 | 9 | 0 | 0 / 0 |
| @jsquash/jxl 1.3.0 | encode | 42 / 45 | 3 | 0 | 0 | 0 | 0 / 0 |
| @jsquash/jxl 1.3.0 | roundtrip | 1 / 1 | 0 | 0 | 0 | 0 | 0 / 0 |
| jxl-oxide-wasm 0.12.6 | decode-lossless | 16 / 16 | 0 | 0 | 0 | 0 | 0 / 0 |
| jxl-oxide-wasm 0.12.6 | decode-lossy | 13 / 13 | 0 | 0 | 0 | 0 | 0 / 0 |
| jxl-oxide-wasm 0.12.6 | encode | 0 / 45 | 45 | 0 | 0 | 0 | 0 / 0 |
| jxl-oxide-wasm 0.12.6 | roundtrip | 0 / 1 | 1 | 0 | 0 | 0 | 0 / 0 |
| wasm-vips 0.0.19 | decode-lossless | 16 / 16 | 0 | 0 | 0 | 0 | 0 / 0 |
| wasm-vips 0.0.19 | decode-lossy | 13 / 13 | 0 | 0 | 0 | 0 | 0 / 0 |
| wasm-vips 0.0.19 | encode | 45 / 45 | 0 | 0 | 0 | 0 | 0 / 0 |
| wasm-vips 0.0.19 | roundtrip | 1 / 1 | 0 | 0 | 0 | 0 | 0 / 0 |
Native, animation, metadata and access probes
| Library | Feature | Status | Scope / evidence |
|---|---|---|---|
| purejsimage | ordinary wide integer preservation | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | ordinary integer color with floating alpha | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | ordinary Float32 lossless encoding | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | ordinary integer ICC preservation | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | floating VarDCT and linear HDR reference blends | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | native float samples | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | explicit display conversion | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | HDR headroom 1/2/4 | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | extra channel samples and meaning | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | ICC preservation | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | animation encode | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | animation decode | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | animation composition | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | frame timing | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | frame access | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | exact JPEG recompression | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | original-byte reconstruction | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | incremental input (known-length read-at source) | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | region reconstruction | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | reduced resolution | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | early progressive output | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | selective source reads | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| purejsimage | Exif/XMP preservation | not tested | node/browser · unavailable |
| purejsimage | orientation rendering | not tested | node/browser · unavailable |
| purejsimage | arbitrary animation blend modes | not tested | node/browser · unavailable |
| purejsimage | multithread speedup | not tested | node/browser · unavailable |
| purejsimage | lossy native 16-bit encode/decode | not tested | node/browser · unavailable |
| jsquash | native integer above 8-bit | API not exposed | public API inspection · survey.json |
| jsquash | native float | API not exposed | public API inspection · survey.json |
| jsquash | extra channels | API not exposed | public API inspection · survey.json |
| jsquash | animation decode | API not exposed | public API inspection · survey.json |
| jsquash | frame timing | API not exposed | public API inspection · survey.json |
| jsquash | frame access | API not exposed | public API inspection · survey.json |
| jsquash | animation encode | API not exposed | public API inspection · survey.json |
| jsquash | ICC | API not exposed | public API inspection · survey.json |
| jsquash | metadata access | API not exposed | public API inspection · survey.json |
| jsquash | incremental input | API not exposed | public API inspection · survey.json |
| jsquash | early progressive output | API not exposed | public API inspection · survey.json |
| jsquash | reduced resolution | API not exposed | public API inspection · survey.json |
| jsquash | region reconstruction | API not exposed | public API inspection · survey.json |
| jsquash | selective source reads | API not exposed | public API inspection · survey.json |
| jsquash | exact JPEG recompression | API not exposed | public API inspection · survey.json |
| jsquash | original-byte reconstruction | API not exposed | public API inspection · survey.json |
| jsquash | Exif/XMP preservation | not tested | node/browser · unavailable |
| jsquash | orientation rendering | not tested | node/browser · unavailable |
| jsquash | arbitrary animation blend modes | not tested | node/browser · unavailable |
| jsquash | multithread speedup | not tested | node/browser · unavailable |
| jsquash | lossy native 16-bit encode/decode | not tested | node/browser · unavailable |
| oxide | frame timing | incorrect output | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| oxide | animation decode | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| oxide | animation composition | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| oxide | frame access | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| oxide | incremental input (sequential feed) | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| oxide | early progressive output | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| oxide | region reconstruction | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| oxide | ICC access | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| oxide | static encode | API not exposed | public API inspection · survey.json |
| oxide | animation encode | API not exposed | public API inspection · survey.json |
| oxide | native raw integer planes | API not exposed | public API inspection · survey.json |
| oxide | native raw float planes | API not exposed | public API inspection · survey.json |
| oxide | extra channels | API not exposed | public API inspection · survey.json |
| oxide | reduced resolution | API not exposed | public API inspection · survey.json |
| oxide | selective source reads | API not exposed | public API inspection · survey.json |
| oxide | exact JPEG recompression | API not exposed | public API inspection · survey.json |
| oxide | original-byte reconstruction | API not exposed | public API inspection · survey.json |
| oxide | Exif/XMP preservation | not tested | node/browser · unavailable |
| oxide | orientation rendering | not tested | node/browser · unavailable |
| oxide | arbitrary animation blend modes | not tested | node/browser · unavailable |
| oxide | multithread speedup | not tested | node/browser · unavailable |
| oxide | lossy native 16-bit encode/decode | not tested | node/browser · unavailable |
| vips | native float samples | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| vips | native-light HDR | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| vips | animation decode | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| vips | frame timing | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| vips | animation composition | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| vips | frame access | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| vips | ICC access | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| vips | animation encode with exact 3/5 ms delays | incorrect output | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| vips | animation encode at ordinary 30/50 ms delays | verified | node · benchmark/jpegxl/comparison/results/specialized-node.json |
| vips | incremental input | API not exposed | public API inspection · survey.json |
| vips | early progressive output | API not exposed | public API inspection · survey.json |
| vips | reduced-resolution JPEG XL reconstruction | API not exposed | public API inspection · survey.json |
| vips | selective source reads | API not exposed | public API inspection · survey.json |
| vips | exact JPEG recompression | API not exposed | public API inspection · survey.json |
| vips | original-byte reconstruction | API not exposed | public API inspection · survey.json |
| vips | Exif/XMP preservation | not tested | node/browser · unavailable |
| vips | orientation rendering | not tested | node/browser · unavailable |
| vips | arbitrary animation blend modes | not tested | node/browser · unavailable |
| vips | multithread speedup | not tested | node/browser · unavailable |
| vips | lossy native 16-bit encode/decode | not tested | node/browser · unavailable |
| vips | region reconstruction | not tested | node/browser · unavailable |
All four initialized in Node and Chromium, with representative Firefox and WebKit checks. Lossy animation in PureJsImage remains Experimental. Exact 3/5 ms timing is a separate edge case: wasm-vips normalizes it to 100/100 ms; its 30/50 ms control passes. Oxide timing fields did not match the expected timeline; its frame access and composition passed.
Quality and compressed size
1 of 24 PureJsImage/comparator/target comparisons have adequate measured brackets on both sides. The rest remain unresolved. Four diagnostics use SSIMULACRA2 70/80/90 and retain Butteraugli coordinates, monotonicity evidence and missing intervals. Each engine/source has at most six lossy points. Alpha is scored on black and white; comparator default alpha distortion remains visible.
At SSIMULACRA2 80, im26-1030-diagnostic: PureJsImage / vips size is 1.212×. This capped photo does not establish original-size, HDR or transparency quality.



Identical 256 × 192 crop coordinates, shown without rescaling. Result crops use actual nearby measured points, not synthesized target-80 images. Source: lilith, PD-own, public domain dedication in imazen-26. Crop hashes and provenance.
Paired lossless size and warm-time ratios
| Runtime / comparator | Scope / effort | Pairs | Median bytes ratio | Median warm-time ratio |
|---|---|---|---|---|
| chromium / jsquash | capped / 1 | 8 | 1.047 | 11.641 |
| chromium / jsquash | capped / 7 | 8 | 1.242 | 5.586 |
| chromium / jsquash | original / 1 | 5 | 0.993 | 2.922 |
| chromium / vips | capped / 1 | 8 | 1.151 | 25.360 |
| chromium / vips | capped / 7 | 8 | 1.131 | 9.574 |
| chromium / vips | original / 1 | 5 | 1.018 | 6.378 |
| chromium / vips | specialized / 1 | 3 | 0.708 | 1.211 |
| node / jsquash | capped / 1 | 8 | 1.047 | 13.360 |
| node / jsquash | capped / 7 | 8 | 1.242 | 6.742 |
| node / jsquash | original / 1 | 5 | 0.993 | 3.141 |
| node / vips | capped / 1 | 8 | 1.151 | 29.190 |
| node / vips | capped / 7 | 8 | 1.131 | 11.503 |
| node / vips | original / 1 | 5 | 1.018 | 8.387 |
| node / vips | specialized / 1 | 3 | 0.708 | 1.130 |
PureJsImage divided by comparator, verified pairs only. Complete originals, capped diagnostics and precision fixtures stay separate. Equal effort numbers do not mean equal work.
Warm encoding and startup
Representative capped photo, lossless effort 1, Node. Three warm operations after initialization, outputs materialized and independently checked. These are exact-output comparisons, not equal numeric lossy distances.
Oxide exposes no encoding API, so it has no encoding time.
Cold initialization plus first encode
| Library | Status | Cold milliseconds | Peak process RSS, MiB |
|---|---|---|---|
| purejsimage | verified | 773.4 [763.8, 774.9] (n=3) | 210.2 [177.2, 212.3] (n=3) |
| jsquash | verified | 131.0 [124.0, 143.5] (n=3) | 179.7 [179.6, 179.7] (n=3) |
| oxide | API not exposed | unavailable | unavailable |
| vips | verified | 200.0 [197.2, 203.7] (n=3) | 168.2 [167.5, 170.1] (n=3) |
Cold reports separately include initialization and first operation, with three fresh observations for selected cells. Process/worker harness startup and input fetching are excluded. Warm operations reuse initialized modules. Small differences on this interactive host are not durable wins.
Oxide's decoder returns PNG, so its measured workflow includes mandatory PNG export. No raw-decode speed ranking equates that workflow with the other outputs.
Startup assets
Loaded encode/decode toolkit, required JS, WASM and worker assets counted once. Includes the common benchmark worker; these are not minimum decode-only application bundles. Brotli quality 11 is an offline estimate; actual requests used uncompressed no-store responses.
wasm-vips uses concurrency(1), but still starts its pthread runtime and requires cross-origin isolation. Actual repeated worker-script transfers remain in the raw report. Native libjxl is an oracle only and never enters JavaScript runtime comparisons.
Preview I/O and memory
PureJsImage's scaled region and oxide's full-frame PNG preview are different workflows; their latency is not ranked. The report retains first nonempty output, requested source bytes and transferred bytes. First-preview visual fidelity was not independently scored. Inspect actual stages in the local tool.
| Library | Milliseconds | Transferred source bytes | Workflow |
|---|---|---|---|
| purejsimage | 59.9 [59.8, 60.6] (n=3) | 20544.0 [20544.0, 20544.0] (n=3) | HTTP Range source, 32x32 region at scale 2; first nonempty pixel block; no canvas upload; geometry-only preview check |
| jsquash | unavailable | unavailable | Public incremental preview API not exposed |
| oxide | 61.9 [59.1, 66.9] (n=3) | 4096.0 [4096.0, 4096.0] (n=3) | Sequential 4096-byte HTTP chunks; first render exported to PNG; full frame, no reduced-resolution API, no canvas upload |
| vips | unavailable | unavailable | Public incremental preview API not exposed |
Node peak RSS is isolated per subject process and includes the runtime, prepared source, output, WASM and worker infrastructure. It is not encoder-managed storage. WASM linear memory was not separately instrumented. Browser memory is unavailable across the tested isolation combinations. Canvas memory was not measured.
Methodology, uncertainty and downloads
Common inputs are pinned sRGB8 straight-alpha samples with orientation 1; high-depth samples are separate. Lossless validation includes color beneath zero alpha. PureJsImage's default lossy alpha must be exact; other wrappers' public lossy defaults may alter alpha, with that error retained. Native float probes and headroom 1/2/4 are not a perceptual HDR compression study.
Missing brackets, API boundaries, untested features and reference-limited comparisons remain visible. These inspected development sources are regression evidence, not an unseen holdout. The full comparison was rerun after the sample, ICC/HDR, animation and native crop updates. The dated library survey retains the pinned comparator versions.
- Complete generated dataset
- Methodology and reproduction commands
- Tested package manifests and hashes
- Pinned input manifests
- Dated official-source survey and versions
- cold-chromium.json SHA-256 b937d29f42c19fcbc4c0babbfd20e00f9005a714c9d3565bc30456ef09b384fd
- cold-node.json SHA-256 c45ac0b8a33ac9f1972eddae6660174bb1d52f1acd2a5f8c62bdda1246d93014
- compat-firefox.json SHA-256 caf79f37c7a770a5d7eb12fb7bd14f6b319204b55dcff73963cc1486e1053e49
- compat-webkit.json SHA-256 08706e549e5a07462c7475b6cbbb097fd51c2c6db5f7183ddf2667b3a66dde61
- main-chromium.json SHA-256 224cc5b585f649b096a23e820e7584d87a865f36f23687c4eaa5de54980349af
- main-node.json SHA-256 57f25253f045b1898751bf00c7f706d23c471a911e960dd7da75b943573b5b03
- preview-chromium.json SHA-256 f847b96ce0d862bd472d7799ded172f155e16bb79d814ef9363fdf4b423300cd
- quality-correctness.json SHA-256 6d15643c104f811c6dd2172d3613a87d2decdec9848676fa625f7898d084ab20
- quality.json SHA-256 4dff4b22d8f1a3218e469e01cfb08bec87e5b27c4033b240f6b0f7010a30c688
- repair-chromium.json SHA-256 aecd6766900053ec1793efa95dadd9efa94d79599027915803ffe8e583476abc
- specialized-node.json SHA-256 0a27556c9e74d550e64531a6820f438b43deb38df27c3b4f1173036611ece8a3
- validation-amendment.json SHA-256 17aa9f0e3ddd943fca0b3dd13a4dd57c7817a80453a5a1cfbf86f8f2f9c36ea5