Why Chromium selected a Rust decoder, how it was integrated, and how it reached default enablement.
Status: โ Enabled by Default in Chrome 155
Starting with Chrome Canary >=155.0.8040.0, JPEG XL decoding and image/jxl advertising are enabled without changing a flag. The default-enable CL has landed. Read the one-year follow-up for the hardening, fuzzing, and parallel-decoding work that prepared the rollout.
The Story
In October 2022, Google removed JPEG XL from Chromium, citing "insufficient ecosystem interest."
In November 2025, Chrome's Architecture Tech Leads announced: "We would welcome contributions to integrate a performant and memory-safe JPEG XL decoder in Chromium."
What changed?
- Safari shipped JPEG XL โ Apple implemented the format
- Firefox updated their position โ Mozilla signaled support pending a Rust decoder
- PDF standardization โ JPEG XL designated as the preferred format for HDR in PDF
- Developer demand โ Bug upvotes, Interop proposals, and survey data
Why JPEG XL?
- 30-50% better compression than JPEG at equivalent quality
- Lossless JPEG transcoding with ~20% size reduction
- Progressive decoding โ images load incrementally
- Modern capabilities โ HDR, wide gamut, alpha, animations
- Cross-browser momentum โ Safari ships it, Firefox is working on it
Implementation
Phase 1: C++ (Abandoned)
Initial approach used libjxl in C++. Feature complete with animation support, but feedback requested Rust for memory safety.
ABANDONED CL 7170439 โ C++ implementation (superseded by Rust)
Phase 2: Rust (Current)
Pivoted to jxl-rs, a pure Rust decoder. Memory-safe and aligned with Chromium's direction.
MERGED CL 7201443 - Add jxl-rs to third_party
Blink integration:
- MERGED CL 7320482 โ Add JXL infrastructure: enums and build flag
- MERGED CL 7319379 โ Add JXL image decoder using jxl-rs
- MERGED CL 7184969 - Wire up the JXL decoder
The Rust decoder also needed performance work for browser workloads. Representative measurements from that period:
| Image | Before | After | C++ libjxl | Change |
|---|---|---|---|---|
| bike (2048ร2560) | 265ms | 198ms | 170ms | +34% |
| progressive (4064ร2704) | 694ms | 560ms | 450ms | +24% |
| blendmodes (1024ร1024) | 115ms | 85ms | 266ms | +35% |
Selected upstream jxl-rs changes
SIMD Optimizations:
- MERGED #585 SIMD table lookup with shuffle
- MERGED #537 SIMD F32 to U8/U16 conversions
- MERGED #536 SIMD transfer functions
- MERGED #535 SIMD upsampling (2x, 4x, 8x)
- MERGED #533 SIMD min and store_interleaved
- MERGED #532 SIMD noise convolution
- MERGED #530 SIMD chroma upsampling
- MERGED #529 SIMD YCbCr to RGB
Performance:
- MERGED #600 SinglePropertyLookup for table-based routing
- MERGED #583 Optimize EPF sigma for modular encoding
- MERGED #581 Cache default quantization tables
- MERGED #538 Flattened modular trees
- MERGED #526 Weighted predictor cache locality
- MERGED #525 Cache natural coefficient orders
- MERGED #524 Precompute cosines in spline rendering
API & Integration:
- MERGED #594 Bump version to 0.2.2
- MERGED #587 Bump version to 0.2.1
- MERGED #586 Fix rendering bugs
- MERGED #574 Add premultiply_output option
- MERGED #556 FFI-friendly API for Chromium
- MERGED #540 Benchmark CI stage
- MERGED #496 Remaining decoder API methods
- MERGED #495 Preview frame API
- MERGED #494 Fix spline DISTANCE_EXP
- MERGED #493 Fix rect bounds check
- MERGED #492 Remove zerocopy dependency
- MERGED #491 HDR tone mapping
- MERGED #533 Add min and store_interleaved
Current Status
โ
Standard image decoding
โ
ICC color profiles
โ
Animations (multi-frame)
โ
Alpha/transparency
โ
Wide gamut (Display-P3)
โ
HDR support (PQ/HLG)
Chromium rolls:
- MERGED Roll jxl 0.2.1 โ 0.2.2
- MERGED Roll jxl 0.2.0 โ 0.2.1
- MERGED Roll jxl 0.1.5 โ 0.2.0
Testing Without Native Support
The jxl-rs-polyfill provides a WASM fallback for testing JPEG XL on browsers without native decoding.
Basic usage:
<script src="https://cdn.jsdelivr.net/npm/jxl-rs-polyfill/dist/auto.js"></script>
The script detects native support before installing its fallback. It covers common <img>, CSS background, <picture>, and SVG cases; the WASM payload is approximately 540 KB gzipped.
More usage examples โ
NPM:
npm install jxl-rs-polyfill
import { JXLPolyfill } from 'jxl-rs-polyfill';
const polyfill = new JXLPolyfill();
await polyfill.start();
Programmatic decoding:
import { decodeJxlToPng } from 'jxl-rs-polyfill';
const pngBytes = await decodeJxlToPng(jxlBytes);
Live Demo
JXL animation support โ no browser supports JXL animations natively yet, so this uses the WASM polyfill:
Acknowledgments
Special thanks to Luca Versari (veluca93) for reviewing PRs and managing jxl-rs releases. The collaboration from jxl-rs maintainers made this possible.
Resources: