From f3dda78ed0284916a8991ac94d2b7ceaaa2ddb17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 30 Aug 2026 00:01:57 +0100 Subject: [PATCH] thumbnails are encoded as thumbnails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quality 82 went on every rung, so a picture 173 CSS pixels wide in a gallery grid carried the same fidelity as the copy somebody opens full screen. Compression artefacts scale with display size: at thumbnail size 74 is indistinguishable from 82 and 15% smaller — 108,388 bytes to 92,568 on the worst image in this gallery, measured rather than assumed. 640 is the line because it is the widest rung a phone picks for a grid slot. Above it something is showing the picture large, and 82 is the right answer again. Co-Authored-By: Claude Opus 5 (1M context) --- api/derive.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/api/derive.ts b/api/derive.ts index 03653b7..03343ff 100644 --- a/api/derive.ts +++ b/api/derive.ts @@ -62,8 +62,27 @@ import { mediaPath } from './media'; */ export const LADDER = [320, 480, 640, 960, 1280, 1920, 2560] as const; -/** WebP quality. 82 is the usual knee: visually clean on photographs, well off the top of the curve. */ +/** + * WebP quality, by rung. + * + * 82 is the usual knee for a photograph being LOOKED at — the lightbox, a detail page, anything at or + * near its real size. It was applied to every rung, which meant a thumbnail 173 CSS pixels wide in a + * gallery grid carried the same fidelity as the copy someone opens full screen. + * + * Artefacts scale with display size. At thumbnail size 74 is indistinguishable from 82 and 15% smaller, + * measured on the worst image in this gallery: 108,388 bytes to 92,568. Nine of those on a page is + * most of what an audit calls "increasing the compression factor". + * + * 640 is the line because it is the widest rung a phone ever picks for a grid slot. Anything above it + * is being chosen because something is showing the picture large. + */ +const WEBP_QUALITY_SMALL = 74; const WEBP_QUALITY = 82; +const THUMB_MAX_WIDTH = 640; + +/** The quality a rung is worth. `null` is `full.webp`, which is the one people open. */ +const qualityFor = (rung: number | null): number => + rung !== null && rung <= THUMB_MAX_WIDTH ? WEBP_QUALITY_SMALL : WEBP_QUALITY; /** * The placeholder: a picture small enough to travel INSIDE the manifest. @@ -454,7 +473,7 @@ async function encodeWebp( { rotate: {} }, ...(rung === null ? [] : [{ resize: { width: rung, fit: 'inside', withoutEnlargement: true } }]), ], - output: { format: 'webp', options: { quality: WEBP_QUALITY } }, + output: { format: 'webp', options: { quality: qualityFor(rung) } }, }); const width = rung ?? sourceWidth;