perf(images): WebP banners, text-only related cards, resized list thumbnails
Deploy / build (push) Successful in 56s
Deploy / build (push) Successful in 56s
Measured before/after with headless Chrome + CDP (not by eye):
- article banner: <picture> WebP q78 with the PNG as fallback, because the PNG
must stay for og:image (social scrapers do not reliably render WebP cards).
70 banners: 6.02 MB -> 2.52 MB (42%). JPEG measured *larger* than PNG here
(q85 = 129%) — the art is flat terminal-style with sharp text edges.
- post page related cards: dropped four 1200x630 PNGs (183 KB) to display
526x276 where the embedded code is illegible; now text-only cards with a
category badge, date and reading time.
- list thumbnails (home + archive): new /og/thumb/<name>-{400,800}.webp served
via srcset/sizes. Rows display at ~398px but were pulling full 1200x630 PNGs.
Homepage thumbnails ~450 KB -> 62 KB; archive (71 rows) ~3.2 MB -> 436 KB.
- desktop banner capped at 720px centred: 67% -> 45% of a 900px viewport,
keeping the art intact instead of cropping it.
Post page totals: images 277 KB -> 38 KB; page height 5758 -> 5065 (desktop)
and 9423 -> 8807 (mobile). Build: 163 pages.
Script: scripts/optimize-images.py; policy: docs/design-guide.md §3; runbook
step added to docs/ops-runbook.md.
This commit is contained in:
1 parent
9462ab2373
commit
f364084284
219 files changed
+158
-15
No files matched your search
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the derivative images the site serves in-page.
|
||||
|
||||
Three outputs, all derived from the existing PNGs:
|
||||
|
||||
1. `public/banners/<slug>.webp` article banner, q78 (~42% of the PNG)
|
||||
2. `public/og/thumb/<name>-800.webp` list thumbnail @2x, q72
|
||||
3. `public/og/thumb/<name>-400.webp` list thumbnail @1x, q72
|
||||
|
||||
Why not JPEG anywhere: this art is flat terminal-style with sharp text edges, and it
|
||||
measured *larger* as JPEG q85 than as PNG (129%); WebP came out at 42%. The PNGs stay
|
||||
the source of truth for `og:image` (social scrapers do not reliably render WebP share
|
||||
cards), and the banner PNG doubles as the <picture> fallback.
|
||||
|
||||
Usage:
|
||||
uv run --with pillow python scripts/optimize-images.py # everything
|
||||
uv run --with pillow python scripts/optimize-images.py <slug> ... # one post
|
||||
"""
|
||||
import glob, os, sys
|
||||
from PIL import Image
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
BANNERS = os.path.join(ROOT, "public", "banners")
|
||||
OG = os.path.join(ROOT, "public", "og")
|
||||
THUMBS = os.path.join(OG, "thumb")
|
||||
BANNER_Q = 78
|
||||
THUMB_Q = 72
|
||||
|
||||
def save_webp(im, path, quality):
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
if im.mode not in ("RGB", "RGBA"):
|
||||
im = im.convert("RGBA" if "A" in im.mode else "RGB")
|
||||
im.save(path, "WEBP", quality=quality, method=6)
|
||||
return os.path.getsize(path)
|
||||
|
||||
def banner(slug):
|
||||
png = os.path.join(BANNERS, slug + ".png")
|
||||
if not os.path.exists(png):
|
||||
return None
|
||||
return os.path.getsize(png), save_webp(Image.open(png), png[:-4] + ".webp", BANNER_Q)
|
||||
|
||||
def thumbs(slug):
|
||||
png = os.path.join(OG, slug + ".png")
|
||||
if not os.path.exists(png):
|
||||
return None
|
||||
im = Image.open(png)
|
||||
src = os.path.getsize(png)
|
||||
out = []
|
||||
for w in (400, 800):
|
||||
h = round(w * 630 / 1200)
|
||||
out.append(save_webp(im.resize((w, h), Image.LANCZOS),
|
||||
os.path.join(THUMBS, f"{slug}-{w}.webp"), THUMB_Q))
|
||||
return src, out
|
||||
|
||||
targets = sys.argv[1:]
|
||||
slugs = targets or sorted(os.path.basename(p)[:-4] for p in glob.glob(os.path.join(OG, "*.png"))
|
||||
if not p.endswith("og-default.png"))
|
||||
|
||||
b_png = b_webp = 0
|
||||
t_png = t_400 = t_800 = 0
|
||||
for s in slugs:
|
||||
b = banner(s)
|
||||
if b:
|
||||
b_png += b[0]; b_webp += b[1]
|
||||
t = thumbs(s)
|
||||
if t:
|
||||
t_png += t[0]; t_400 += t[1][0]; t_800 += t[1][1]
|
||||
|
||||
n = max(1, len(slugs))
|
||||
print(f"banners : {len(slugs)} files {b_png/1024/1024:.2f} MB PNG -> {b_webp/1024/1024:.2f} MB WebP "
|
||||
f"({100*b_webp/max(1,b_png):.0f}%) [{b_webp/n/1024:.1f} KB/file]")
|
||||
print(f"thumbs : 400w {t_400/1024:.0f} KB ({t_400/n/1024:.1f} KB/file) + 800w "
|
||||
f"{t_800/1024:.0f} KB ({t_800/n/1024:.1f} KB/file) vs PNG {t_png/1024/1024:.2f} MB "
|
||||
f"({100*(t_400+t_800)/max(1,t_png):.0f}% of PNG)")
|
||||
Reference in new issue
Block a user