Published on

JPEG XL (.jxl) in Chrome 155+ — Empirical Compression Benchmark Across 12 JPEG Profiles

Authors
  • avatar
    Name
    Hoang Nguyen
    LinkedIn
JPEG XL (.jxl) Benchmark — Chrome 155+ & Cloud Storage

For over 30 years, JPEG (.jpg) has been the default image format of the internet. When JPEG XL (.jxl) first emerged, it promised something almost too good to be true for infrastructure and storage engineers: recompressing existing .jpg files by 20–30% with zero quality loss and restoring the exact original file byte-for-byte whenever needed.

In late 2022 (Chrome 110), Google dropped experimental C++ libjxl support due to memory-safety concerns, stalling web adoption. In October 2026 (Chrome 155+), Google officially reversed course — shipping native .jxl decoding enabled by default, powered by jxl-rs, a memory-safe pure Rust implementation with custom SIMD acceleration.

To evaluate how JPEG XL actually performs in real production workloads, we built a 12-category JPEG benchmark corpus (5.62 MB total) and tested it across Lossless DCT Transcoding, Lossy JXL (Q80), WebP (Q80), and RGB Pixel Lossless.


Executive Summary: JPEG XL in 5W1H

DimensionBreakdown
WHAT (What is it?)JPEG XL (.jxl) — a next-generation, royalty-free image codec designed to replace legacy JPEG with superior Lossy compression, true Lossless, HDR, animation, and bit-exact Lossless JPEG Transcoding.
WHY (Why does it matter?)Shrinks existing .jpg archives by an average of 32.93% with zero quality loss (100% SHA-256 byte-exact reversible back to .jpg), or cuts 46.93% (up to 65.1%) in Lossy Q80 mode with higher PSNR than WebP.
WHO (Who should care?)SRE / DevOps / Cloud Architects looking to cut AWS S3, Google Cloud Storage, and backup bills immediately; and Frontend / Web Engineers optimizing Core Web Vitals (LCP).
WHERE (Where does it run?)Across Cloud Object Storage (S3/GCS/NAS) and modern browsers: Apple Safari (iOS/macOS) and Google Chrome 155+ (via the Rust jxl-rs decoder).
WHEN (When to adopt?)Immediately for Cloud Storage and archival pipelines. For public web delivery, pair with a smart CDN (CloudFront / Cloudflare) using Accept: image/jxl content negotiation.
HOW (How to implement?)Via libjxl CLI (cjxl / djxl) or Python pillow-jxl-plugin — while carefully controlling the lossless_jpeg flag to avoid a +201% (3x) pixel-bloat trap.

What: The 3 Core Capabilities of JPEG XL

1. Lossless JPEG Transcoding (Bit-Exact Reversibility)

This is the killer feature for SREs and storage engineers. Traditional image conversion decodes a .jpg into raw RGB pixels and re-encodes them into a new format — triggering generation loss (additional compression artifacts) and permanently discarding the original bitstream.

JPEG XL can take an existing .jpg and losslessly recompress its 8x8 DCT coefficients directly without ever decoding to pixels. By replacing legacy 1990s Huffman tables with modern Asymmetric Numeral Systems (ANS) entropy coding and context modeling:

  • File size drops by 14.6% to 46.6% (averaging 32.93% in our benchmark)
  • Zero visual degradation — not a single DCT coefficient is altered
  • 100% Byte-for-Byte Reversible — running djxl image.jxl restored.jpg reproduces the exact original .jpg file with an identical SHA-256 checksum

2. Next-Gen Lossy Compression (VarDCT + Adaptive Quantization)

When encoding from scratch or re-encoding with lossy compression (quality=80), JPEG XL uses variable-size DCT blocks (up to 256x256), a perceptual color space (XYB), adaptive quantization, and dedicated primitives for sharp edges and text (patches and splines). It consistently achieves 1.0 dB to 3.1 dB higher PSNR than WebP at smaller or comparable file sizes, without suffering from WebP's mandatory 8-bit 4:2:0 chroma subsampling blur.

3. Memory-Safe Browser Decoding (jxl-rs)

With Safari (macOS/iOS) and Chrome 155+ (desktop/Android) both supporting native .jxl decoding, the browser compatibility barrier that held JPEG XL back for three years is rapidly disappearing.


Why: Empirical Benchmark Across 12 Real-World JPEG Profiles

Not all JPEGs are created equal. A studio camera original with 4:4:4 chroma behaves completely differently from a progressive MozJPEG web asset or a UI screenshot with sharp text.

We assembled 12 distinct JPEG categories totaling 5.62 MB (5,897,387 bytes) and measured four compression modes:

  1. JXL Lossless Transcode (cjxl --lossless_jpeg=1 -e 7) + djxl SHA-256 verification
  2. JXL Lossy Q80 (quality=80, lossless_jpeg=False)
  3. WebP Lossy Q80 (quality=80, method=4)
  4. JXL Pixel Lossless Trap (lossless=True, lossless_jpeg=False on decoded RGB pixels)
JPEG XL vs WebP Space Saved Across 12 JPEG Profiles

Complete Benchmark Summary Table

#JPEG Test Profile
Original .jpg
Baseline (100%)
JXL Lossless Transcode
(100% SHA-256 Exact)
JXL Lossy Q80
(PSNR)
WebP Lossy Q80
(PSNR)
Pixel Lossless Trap
(Decoded RGB)
01Studio Q95
Baseline · 4:4:4 · 1920x1280
01 Original JPG664.1 KB
100% Original
01 JXL Lossless394.1 KB
-40.66% ✅
231.9 KB (-65.07%)
39.9dB
264.7 KB (-60.14%)
38.9dB
1,201.8 KB
(+81% ❌)
02Camera Q85
Baseline · 4:2:0 · 1920x1280
02 Original JPG400.6 KB
100% Original
02 JXL Lossless271.2 KB
-32.30% ✅
232.9 KB (-41.88%)
39.9dB
279.6 KB (-30.21%)
38.8dB
1,278.0 KB
(+219% ❌)
03Progressive Q85
SOF2 · 4:2:0 · 1920x1280
03 Original JPG333.4 KB
100% Original
03 JXL Lossless271.2 KB
-18.67% ✅
232.9 KB (-30.16%)
39.9dB
279.6 KB (-16.14%)
38.8dB
1,278.0 KB
(+283% ❌)
04Low Quality Q50
8x8 Blocks · 4:2:0 · 1920x1280
04 Original JPG248.8 KB
100% Original
04 JXL Lossless191.6 KB
-23.01% ✅
231.1 KB (-7.13%)
40.1dB
321.7 KB
(+29.29% bloat ❌)
1,342.8 KB
(+440% ❌)
05Portrait Bokeh Q90
Gradients · 4:2:0 · 1500x1500
05 Original JPG229.1 KB
100% Original
05 JXL Lossless142.7 KB
-37.72% ✅
116.6 KB (-49.12%)
44.9dB
74.8 KB (-67.37%)
41.8dB
801.7 KB
(+250% ❌)
06Architecture Q90
High-Freq · 4:2:2 · 1920x1280
06 Original JPG410.7 KB
100% Original
06 JXL Lossless254.8 KB
-37.95% ✅
194.9 KB (-52.54%)
41.2dB
200.8 KB (-51.11%)
39.6dB
1,050.2 KB
(+156% ❌)
07Grayscale Q90
1-Channel L · 1920x1280
07 Original JPG384.8 KB
100% Original
07 JXL Lossless244.3 KB
-36.51% ✅
182.9 KB (-52.48%)
41.4dB
196.3 KB (-48.99%)
40.1dB
850.3 KB
(+121% ❌)
08UI / Sharp Text Q90
Dashboard · 4:4:4 · 1920x1080
08 Original JPG364.4 KB
100% Original
08 JXL Lossless194.6 KB
-46.60% ✅
131.7 KB (-63.86%)
37.7dB
86.7 KB (-76.21%)
35.2dB
465.8 KB
(+28% ❌)
09E-Commerce Q85
White BG · 4:2:0 · 1600x1600
09 Original JPG152.9 KB
100% Original
09 JXL Lossless105.6 KB
-30.92% ✅
69.8 KB (-54.36%)
46.7dB
49.0 KB (-67.97%)
44.3dB
470.9 KB
(+208% ❌)
10Heavy EXIF Q85
~30KB EXIF · 1920x1280
10 Original JPG430.1 KB
100% Original
10 JXL Lossless271.4 KB
-36.90% ✅
262.3 KB (-39.00%)
39.9dB
279.6 KB (-34.99%)
38.8dB
1,307.5 KB
(+204% ❌)
11Web Original
Pre-optimized · 1920x1280
11 Original JPG296.1 KB
100% Original
11 JXL Lossless252.9 KB
-14.60% ✅
232.0 KB (-21.65%)
39.9dB
263.7 KB (-10.95%)
38.9dB
1,236.2 KB
(+317% ❌)
124K UHD Photo
Q90 · 4:2:0 · 3840x2160
12 Original JPG1,844.0 KB
100% Original
12 JXL Lossless1,268.2 KB
-31.22% ✅
937.3 KB (-49.17%)
37.3dB
1,110.9 KB (-39.76%)
37.9dB
6,063.4 KB
(+229% ❌)
∑TOTAL / AVG
All 12 Profiles
5.62 MB
100% Baseline
3.77 MB
-32.93% ✅
2.98 MB (-46.93%) 🏆3.33 MB (-40.84%)16.94 MB (+201.2%) ❌

How: Production Workflows & The Python pillow-jxl Pitfall

Many tutorials recommend automating JPEG XL conversion in Python using Pillow and pillow-jxl-plugin with code that looks like this:

from PIL import Image
import pillow_jxl

img = Image.open("input.jpg")

# ⚠️ TRAP 1: What many tutorials write for Lossy Q80:
img.save("output_q80.jxl", format="JXL", quality=80)

# ⚠️ TRAP 2: What happens if you resize/edit first, then save lossless=True:
resized = img.resize((1200, 800))
resized.save("output_lossless.jxl", format="JXL", lossless=True)

When we ran this in our benchmark, we uncovered two critical pitfalls that every engineer must know before deploying to production:

  1. Silent quality=80 Ignore (lossless_jpeg=True default): When img is opened directly from a .jpg file, pillow-jxl-plugin defaults to lossless_jpeg=True and emits a warning:
    UserWarning: Using JPEG reconstruction to create lossless JXL image from JPEG.
    This is the default behavior for JPEG encode, if you want to disable this, please set 'lossless_jpeg'.
    
    As a result, img.save("output_q80.jxl", format="JXL", quality=80) completely ignores quality=80 and outputs a lossless DCT-transcoded file instead!
  2. The +201.2% (3x) Pixel-Lossless Bloat Trap: Once an image is resized, cropped, or converted to RGB in memory (img.convert("RGB")), the original JPEG DCT bitstream is gone (lossless_jpeg=False). Calling lossless=True at that point forces JPEG XL's Modular encoder to losslessly encode every single RGB pixel — including all the 8x8 JPEG mosquito noise and block artifacts. Across our 12 test files, this bloated total size from 5.62 MB up to 16.94 MB (+201.20%)!

Workflow A: Cloud Storage & S3 Backup (Zero-Loss, 100% Reversible)

If your goal is cutting AWS S3, Google Cloud Storage, or NAS storage costs on existing .jpg repositories while retaining the ability to restore the exact original .jpg byte-for-byte:

# Install libjxl on macOS / Linux
brew install jpeg-xl   # or apt-get install libjxl-tools

# 1. Losslessly transcode JPEG -> JXL (saves ~32.9% storage)
cjxl input.jpg backup.jxl --lossless_jpeg=1 -e 7

# 2. Reconstruct the exact original .jpg (100% SHA-256 match)
djxl backup.jxl restored.jpg
shasum -a 256 input.jpg restored.jpg

Or in Python (pillow-jxl-plugin):

from PIL import Image
import pillow_jxl

# Explicitly use lossless_jpeg=True on untouched JPEG files
with Image.open("input.jpg") as img:
    img.save("backup.jxl", format="JXL", lossless_jpeg=True, effort=7)

Workflow B: Web & CDN Frontend Delivery (Lossy Q80)

If your goal is minimizing bandwidth and improving Largest Contentful Paint (LCP) for web browsers (Chrome 155+, Safari), explicitly set lossless_jpeg=False:

# CLI Lossy Q80 encode
cjxl input.jpg web_q80.jxl -q 80 --lossless_jpeg=0 -e 7
from PIL import Image
import pillow_jxl

# Explicitly disable lossless_jpeg to trigger VarDCT lossy compression at Q80
with Image.open("input.jpg") as img:
    img.save("web_q80.jxl", format="JXL", quality=80, lossless_jpeg=False)

Key Conclusions: What the Numbers Tell Us

  1. Lossless JPEG Transcoding exceeds the "20% rule of thumb" (-32.93% average):
    • Camera/Studio Baseline JPEGs & UI Graphics
    • Pre-optimized Progressive / MozJPEG Web Assets
    • 100% SHA-256 Verified
  2. JXL Lossy (Q80) outperforms WebP (Q80) on both size (-46.93% vs -40.84%) and fidelity
  3. Never run lossless=True on decoded JPEG pixels:

P/S: I love this one and will apply for my blog in the near future 🎉🎁🎊.