Back to blog
TechnicalApril 20, 20265 min readKonvrt Team

Converting RAW Photos to JPEG XL in the Browser

A practical look at decoding CR3, NEF, ARW, and DNG RAW files client-side with WebAssembly and re-encoding them as JPEG XL — no server uploads required.

Converting RAW Photos to JPEG XL in the Browser

A 45 MB Canon CR3 off a full-frame sensor is not something most people want to email around. But uploading it to a random web tool to "just convert it" means sending your untouched, unsigned, uncompressed originals into whatever someone's backend is doing. The browser is now fast enough that you don't have to.

This post walks through how browser-based RAW-to-JXL conversion actually works in 2026 — which decoders handle which manufacturer formats, where JPEG XL fits in, and the rough CPU budget you should expect.

Why JPEG XL for RAW output

JPEG XL (ISO 18181) is the natural target format for processed RAWs. It supports 16-bit channels, wide gamuts (Rec. 2020, Display P3), HDR metadata, and lossless re-encoding of existing JPEGs. Against a 16-bit TIFF export from Lightroom, a visually lossless JXL is typically 60 to 75 percent smaller. Against a high-quality JPEG, it's around 30 percent smaller at the same perceptual quality.

Chrome 145 re-enabled JPEG XL support behind a default-on flag in January 2026. Safari has shipped it since 17.0. Firefox still needs image.jxl.enabled flipped, which is the main reason you'll still want to offer JPEG or AVIF as a fallback export.

The RAW decode problem

RAW is not one format. Each camera manufacturer ships its own container with its own Bayer layout, black-level offsets, white balance coefficients, and lens correction tables:

Format Vendor Container Typical decoder
CR3 Canon ISO BMFF (CRX codec) LibRaw 0.22+
NEF Nikon TIFF/EP LibRaw, dcraw
ARW Sony TIFF/EP LibRaw
DNG Adobe/generic TIFF/EP LibRaw, dng_sdk
RAF Fujifilm (X-Trans) proprietary LibRaw

In the browser, the realistic option is LibRaw compiled to WebAssembly. A recent build of LibRaw 0.22 weighs about 2.1 MB gzipped and handles everything except the most exotic medium-format backs. You feed it an ArrayBuffer, it hands back a 16-bit linear RGB buffer plus EXIF.

const raw = await LibRaw.open(fileBytes);
const { width, height, data } = await raw.processImage({
  outputBps: 16,
  outputColor: "prophoto",
  useCameraWb: true,
});

The data is a Uint16Array in ProPhoto or your chosen working space. From here it goes to the JXL encoder.

Encoding JPEG XL in WASM

Google's libjxl 0.11 has an official Emscripten build. A realistic call for "visually lossless" output looks like:

const encoded = await jxl.encode(data, {
  width,
  height,
  bitsPerSample: 16,
  colorspace: "prophoto",
  distance: 1.0,     // 0 = lossless, 1.0 ~ visually lossless
  effort: 7,
});

Effort 7 is the practical ceiling for interactive use. Effort 9 yields maybe 2 to 4 percent more compression at 3 to 4x the CPU time — not worth it unless you're archiving.

CPU budget on real hardware

Measured on a 24 MP Sony ARW (roughly 48 MB file) through a single decode plus encode pass:

CPU Decode (LibRaw WASM) JXL encode e=7, d=1.0 Total
M2 Pro 0.9 s 2.6 s 3.5 s
M4 0.6 s 1.7 s 2.3 s
Ryzen 7 7840U 1.1 s 3.4 s 4.5 s
i5-1240P 1.4 s 4.2 s 5.6 s

Workers matter here. Main-thread decoding will jank the page for several seconds. Push LibRaw and libjxl into a dedicated Worker, use Transferable for the pixel buffer, and keep the UI responsive.

Handling big shoots

A wedding card dump of 800 RAWs is not something you want to babysit one file at a time. The batch converter parallelises across navigator.hardwareConcurrency - 1 workers and streams results into a zip using OPFS so you never hold all 800 decoded buffers in memory at once. On an M4 with 10 performance cores, 800 ARWs finished in about 31 minutes end to end.

For one-offs or a few hero frames, the single-file converter is faster to start — no queue to set up, drop file, save output.

Gotchas

  • Lens correction: LibRaw applies per-camera corrections only if the maker notes are parsed correctly. CR3 maker notes were flaky until LibRaw 0.21.3 — pin a newer version.
  • Color management: Browsers apply the display profile at paint time, so don't double-convert. Export tagged ProPhoto or Display P3, let the OS handle the rest.
  • Memory ceiling: 32-bit WASM caps at ~4 GB. A 100 MP Fujifilm GFX RAF decoded to 16-bit RGBA is about 800 MB of buffer — you can do it, but only one at a time.

RAW-to-JXL in the browser is no longer a party trick. On current laptop hardware, client-side is competitive with a round trip to any hosted converter, and your originals never leave the device. If you want to compare JXL output against AVIF or WebP before committing, there's a format comparison here.

Built for fast file workflows

Convert, optimize, and ship files without sending them away first.

Konvrt keeps the experience simple: local-first processing when possible, clear pricing, strong privacy defaults, and focused tools for repetitive file work.

Local-first

Files stay on your device for supported browser workflows.

Fast answers

Use FAQ, docs, and contact paths without hunting around the site.

Clear upgrades

Move from free workflows to paid access without confusing plan language.