Files
Oxicloud/benches/WEBP.md
T

85 lines
4.5 KiB
Markdown
Raw Normal View History

# WebP thumbnails — codec comparison (bandwidth vs quality)
WebP (lossy) is the **primary** thumbnail codec: generated eagerly on upload and
served to the ~97% of clients that advertise `Accept: image/webp`. JPEG is the
lazy fallback for older clients and NextCloud. This doc records the before/after
that justified the change and the `WEBP_QUALITY` choice.
## What it buys
For "hundreds of photos", thumbnail **bytes on the wire** are the dominant cost.
WebP at q82 ships visually-equivalent thumbnails (SSIM within ~0.005 of JPEG q80,
imperceptible at thumbnail scale) for **~65% fewer bytes** on the bench corpus.
> ⚠️ **Honest caveat — the corpus is smooth.** The synthetic corpus is now
> photo-realistic (summed low-frequency sinusoids = smooth color fields + mild
> grain), which compresses far more like a real photo than the old white-noise
> corpus did. But it has **no hard edges / text / foliage** — exactly the
> high-frequency content where both codecs grow and the WebP-vs-JPEG ratio
> narrows toward the classic **~25–40%**. So treat ~65% as an upper bound and
> ~25–40% as the realistic real-photo expectation. Drop real photos into
> `benches/corpus/` (same filenames) to measure on real data.
## Reproduce
```bash
cargo run --release --features bench --example bench_thumbnails_mem
# Tables E1 (WebP quality sweep) and E2 (production codec) at the bottom.
```
SSIM is mean over non-overlapping 8×8 luma blocks vs the **uncompressed**
full-decode source resized to the thumbnail's exact dims (`reference_luma_at`),
so it isolates codec fidelity (no second lossy step). Note this 8×8 metric
slightly favours JPEG's 8×8 DCT blocks, so WebP's SSIM reads a hair low.
## E2 — production codec at `WEBP_QUALITY = 82` (14 cores)
| case | size | jpeg B | webp B | save% | ssim jpeg | ssim webp |
|-----------|---------|-------:|-------:|------:|----------:|----------:|
| jpeg_12mp | Icon | 4608 | 1966 | 57.3% | 0.9960 | 0.9916 |
| jpeg_12mp | Preview | 17564 | 6614 | 62.3% | 0.9931 | 0.9871 |
| jpeg_12mp | Large | 47254 | 15580 | 67.0% | 0.9865 | 0.9808 |
| jpeg_24mp | Icon | 5518 | 2588 | 53.1% | 0.9970 | 0.9935 |
| jpeg_24mp | Preview | 19294 | 7556 | 60.8% | 0.9949 | 0.9904 |
| jpeg_24mp | Large | 51705 | 18904 | 63.4% | 0.9918 | 0.9863 |
| jpeg_48mp | Icon | 4554 | 1870 | 58.9% | 0.9957 | 0.9895 |
| jpeg_48mp | Preview | 16567 | 5704 | 65.6% | 0.9930 | 0.9866 |
| jpeg_48mp | Large | 45938 | 12540 | 72.7% | 0.9910 | 0.9866 |
**Total: JPEG 213.0 KB → WebP 73.3 KB = 65.6% smaller.** WebP SSIM trails JPEG by
≤0.006 everywhere — imperceptible at thumbnail scale.
Encode (Preview/12 MP, full pipeline incl. the shared decode): **JPEG 34.4 ms vs
WebP 39.5 ms** (+5 ms, +15%). The WebP encoder is marginally slower but the cost
is paid once, eagerly, in the background generator — it never sits in the
request path (served thumbnails are cache hits).
## E1 — why q82 (quality sweep, Preview/400px)
Even at q90 WebP's 8×8-block SSIM stays a touch under JPEG q80 (the metric favours
JPEG's DCT grid), but the gap is ≤0.008 at the 0.99 level while the byte savings
are 50–68%. q82 is the chosen balance: SSIM 0.987–0.990 (within ~0.005 of JPEG,
imperceptible) at ~60–66% fewer bytes.
| source | JPEG q80 (B / ssim) | webp q78 | webp q82 | webp q86 | webp q90 |
|-----------|---------------------|-------------|-------------|-------------|-------------|
| jpeg_12mp | 17564 / 0.9931 | −65% 0.9858 | −62% 0.9871 | −58% 0.9900 | −51% 0.9912 |
| jpeg_24mp | 19294 / 0.9949 | −64% 0.9891 | −61% 0.9904 | −57% 0.9921 | −50% 0.9930 |
| jpeg_48mp | 16567 / 0.9930 | −68% 0.9855 | −66% 0.9866 | −61% 0.9888 | −55% 0.9911 |
Tune `WEBP_QUALITY` (in `thumbnail_service.rs`) up for more fidelity, down for
more bandwidth savings.
## How it's served (Strategy B)
- **Eager**: on upload the background generator renders all 3 sizes as WebP
(`{blob_hash}.webp`).
- **Lazy fallback**: a request without `Accept: image/webp` (or NextCloud, which
pins JPEG) generates `{blob_hash}.jpg` on first hit, then caches it like WebP.
- **Negotiation**: `GET /api/files/{id}/thumbnail/{size}` reads `Accept`,
serves WebP or JPEG, sets `Vary: Accept` on every response (incl. 304) and a
format-keyed ETag so shared caches never hand the wrong codec to a client.
`Content-Type` is byte-sniffed (`infer`), so it always matches the bytes.
- Dedup, the moka cache (keyed by `(file_id, size, format)`), and cleanup all
carry both formats.