Skip to content

perf(docs): re-encode the hero cover to WebP and hoist its path into one constant - #12377

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-12242-hero-png-weight
Aug 25, 2026
Merged

perf(docs): re-encode the hero cover to WebP and hoist its path into one constant#12377
os-zhuang merged 1 commit into
mainfrom
claude/issue-12242-hero-png-weight

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#12242

The homepage's click-to-play video poster was a 2400×1200 PNG. It is now a WebP re-encode of the same master, and the path all three consumers spell is one exported constant.

Before / after, measured over the wire

Both numbers come from next build && next start in this worktree (port 3242), not from ls and not from production — production has been serving one unchanged deployment since ~16:55 (#12333), so the live site is evidence about a stale build only.

beforeafter
path/hero-cover-dark.png/hero-cover-dark.webp
Content-Typeimage/pngimage/webp
Content-Length / curlsize_download406,703 B83,272 B
Content-Encodingnonenone
Chrome Resource Timing transferSize83,572 B (83,272 body + 300 headers)
share of the PNG100%20.5% — 79.5% off
$ curl -sI -H 'Accept-Encoding: gzip, deflate, br, zstd' http://127.0.0.1:3242/hero-cover-dark.webp
HTTP/1.1 200 OK
Content-Type: image/webp
Content-Length: 83272
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3242/hero-cover-dark.png
404

The 404 is the reverse check: the PNG is gone from the build output, so nothing can still be quietly reading it.

Displayed size — the card's "~1024 px" re-derived

Read off the running page in Chrome rather than inferred from the Tailwind classes:

displayedCssW 974 displayedCssH 486 devicePixelRatio 2 deviceW 1948
naturalWidth 2400 naturalHeight 1200

max-w-5xl (64rem) minus px-6 padding pins the figure at 974 CSS px at every viewport ≥ 1024 px — checked at 1280, 1440 and 2560 and it does not move. So the widest a 2× desktop ever needs is 1948 device px; the card's "~1024 px" is close but the exact number is 974. The asset stays 2400 px wide, which is 1.23× more than a 2× display consumes.

Keeping 2400×1200 is deliberate: og:image:width / og:image:height are emitted from the same constant, and a declared size that disagrees with the bytes is worse than no declaration. They did not have to move because the dimensions did not.

Fidelity — what was actually compared

Encode: sharp('docs/screenshots/hero-cover-dark.png').webp({ quality: 80, effort: 6 }), sharp 0.35.3 / libwebp 1.6.0. Reproducible — re-running it produces 83,272 B again.

Both images were decoded to raw at native size, then resampled Lanczos3 to 1948×974 — the exact device-pixel box measured above — and compared on the luma plane:

value
SSIM (8×8 windows)0.994713
PSNR47.10 dB
max per-pixel Δ24 / 255
pixels with |Δ| > 50.739%
control: master vs. master through the same pipelineSSIM 1.000000, PSNR ∞

The control matters. A first pass scored a lossless WebP of this PNG at SSIM 0.9936 against the PNG it was losslessly encoded from — because sharp(file).resize(...) hands the target size to the codec (libwebp's rescaler, libjpeg shrink-on-load), so each format was being downscaled by a different resampler. That pass was ranking rescalers, not codecs. Decoding to raw first fixed it, and the lossless control now reads exactly 1.0.

Beyond the aggregate, 32×32 tiles were scored across the rendered frame and the four regions most at risk were cropped and inspected side by side at 2× magnification of the 2× render — the code panel (syntax-coloured monospace on dark, the classic ringing target), the smallest text on the image (the dashboard sub-labels), the purple gradient headline (banding target), and the two worst-scoring tiles. Original, q75, q80, q85 and q90 are indistinguishable in all four. The worst-scoring tiles turn out to be near-uniform white chart areas where SSIM is hypersensitive to tiny absolute differences, not places where anything is visibly wrong.

Quality 80 was chosen over 85 (93.6 KB) because it clears the ~100 KB bar with margin rather than grazing it, and the visual comparison could not separate them.

WebP rather than AVIF, deliberately. AVIF is measurably better per byte on this image — q60 reaches SSIM 0.998 at 65 KB, beating WebP q85 at 94 KB. It was rejected anyway: this is a plain <img> with no <picture> fallback and it is the og:image, so it has to decode in every browser and every unfurler. Two independent published test matrices agree that WebP now works across the major unfurlers and AVIF works in roughly a third of them. The constraint is recorded next to the constant so the next person does not re-litigate it from the byte table alone.

One constant, three call sites

HERO_COVER now lives in apps/docs/lib/site.ts beside SITE_ORIGIN / absoluteUrl(), and carries the re-encode recipe, the provenance of the master, and why the dimensions and format are constrained. The three literals that used to spell the path:

call sitewasnow
app/[lang]/page.tsx — video posterposter="/hero-cover-dark.png"poster={HERO_COVER.url}
app/[lang]/page.tsxHOME_CARD (OG + Twitter + JSON-LD image)local object literalHERO_COVER (constant deleted)
app/[lang]/blog/[[...slug]]/page.tsxBLOG_CARD (index + every post, OG + Twitter + BlogPostingimage)local object literalHERO_COVER (constant deleted)

Verified in the served HTML rather than in the source:

homepage <img src="/hero-cover-dark.webp" …>
homepage og:image / twitter:image = https://objectstack.ai/hero-cover-dark.webp
og:image:width 2400 · og:image:height 1200
/blog og:image / twitter:image = https://objectstack.ai/hero-cover-dark.webp
/blog/… og:image and BlogPosting "image" = https://objectstack.ai/hero-cover-dark.webp

A repo-wide grep leaves no /hero-cover-dark.png URL anywhere; the only remaining .png spellings are the README's own <img src="docs/screenshots/hero-cover-dark.png"> and prose in two docblocks pointing at that master.

Out-of-surface edit, declared

apps/docs/components/youtube-embed.tsx is outside the dispatched file surface. One clause of its docblock said the cover is docs/screenshots/hero-cover-dark.png "copied into public/" — this change makes that false (it is a re-encode now, not a copy), in the file that documents this exact asset. Comment-only, one sentence, no behaviour: it now says re-encoded and points at HERO_COVER. No open PR claims that file (all 100 open PRs enumerated against my file surface: zero hits).

docs/screenshots/hero-cover-dark.png — the README's copy still resolves

Untouched, and proved rather than asserted:

$ git rev-parse HEAD:docs/screenshots/hero-cover-dark.png d8322c8392b2839baec5bf232f315b6b73caaaad
$ git hash-object docs/screenshots/hero-cover-dark.png d8322c8392b2839baec5bf232f315b6b73caaaad
$ git diff HEAD -- docs/screenshots/ README.md (empty)
$ shasum -a 256 … 6ab3f83c…065fe9 (identical to pre-change)

Still tracked, still 406,703 B, still at the path the README's relative <img src> resolves to from the repo root. It is now also the master this PR's WebP is derived from, which gives it a second reason to stay.

Verification

At the pushed head 63e50a103, tree clean:

checkverdict line
pnpm --filter @objectstack/docs typecheckVERDICT command-exit 0
next build (apps/docs)✓ Compiled successfully, VERDICT command-exit 0
pnpm check:docs-locale-catch-allVERDICT command-exit 0
pnpm check:page-declaration-shapeVERDICT command-exit 0
pnpm check:published-filesVERDICT command-exit 0
pnpm check:test-source-aliasVERDICT command-exit 0
pnpm check:type-source-resolutionVERDICT command-exit 0
pnpm check:nul-bytesVERDICT command-exit 0

The five check:* families are what node scripts/pm/dispatch-gates.mjs derives for this diff's six paths (it reads the change set from git itself); check:nul-bytes is owed by any edit.

Verification ran UNLOCKED, declared.scripts/pm/os-verify-lock.sh reports no usable flock on this host — the shared verify lock is Linux-only and this is macOS — so every command above ran through the entry point but with no mutual exclusion. Each ends VERDICT … · UNLOCKED (declared).

Repo-wide pnpm lint is NOT MEASURED here, and narrowed lint is declared in its place.pnpm lint (eslint . --no-inline-config) died twice with a V8 heap OOM on this host — FATAL ERROR: Ineffective mark-compacts near heap limit, exit 134, at the default heap and again at --max-old-space-size=6144. That is a host failure, not a red gate, and CI runs the full scan regardless. The narrowing is measured, not assumed:

  1. Receiving population read from ESLint's own config, not guessed: ESLint#isPathIgnored says all four changed source files are in scope and both changed binaries (.png, .webp) are ignored.
  2. File count read from --format json: exactly 4 files reported linted, 0 errors, 0 warnings, exit 0 — so nothing was silently skipped.
  3. Config invariance for untouched files: calculateConfigForFile returns parserOptions.project: null, projectService: null, EXPERIMENTAL_useProjectService: null for every probed file, inside and outside apps/docs, and the flat config contains no such switch at all. With no type-aware linting, no rule's verdict on a file I did not touch can depend on this diff — so the narrowing excludes nothing.

No changeset: docs-site only, @objectstack/docs is private: true and this publishes nothing. skip-changeset applied.


Generated by Claude Code

…one constant
The homepage video poster was a 2400x1200 PNG served at 974 CSS px. Measured
over the wire against a local `next build && next start`: 406,703 B, no
transport compression (image/png, Content-Length == file size). It is the
heaviest single element the homepage loads.
Re-encoded from the master `docs/screenshots/hero-cover-dark.png` to WebP at
quality 80 / effort 6 (sharp 0.35.3, libwebp 1.6.0), keeping 2400x1200 so the
declared `og:image:width` / `og:image:height` stay true and 2x displays stay
oversampled. 83,272 B on the wire -- 20.5% of the PNG, and 83,572 B including
response headers as Chrome's Resource Timing reports it.
Fidelity, measured at the exact device-pixel size Chrome paints (1948x974,
read off the running page): SSIM 0.994713, PSNR 47.10 dB against the master,
max per-pixel delta 24/255, 0.739% of pixels differing by more than 5/255. The
same harness scores the master against itself at SSIM 1.0 / PSNR infinity, so
it is not fabricating agreement. Crops of the four regions most at risk --
code panel, smallest dashboard labels, gradient headline, and the two
worst-scoring tiles -- are indistinguishable at 2x magnification.
WebP rather than AVIF deliberately: this is a plain `<img>` with no `<picture>`
fallback and it doubles as the og:image, so it must decode everywhere. AVIF is
measurably better per byte on this image (q60 reaches SSIM 0.998 at 65 KB) and
was rejected because unfurler support is not there.
The path was spelled three times -- the poster, the homepage card, the blog
card -- and two of those read as SEO config rather than image references. It is
now `HERO_COVER` in `lib/site.ts`, next to `SITE_ORIGIN`, carrying the
re-encode recipe and the reason the dimensions and the format are constrained.
`docs/screenshots/hero-cover-dark.png` -- the README's copy, a separate file
for a separate consumer -- is untouched: same blob sha before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@os-zhuangos-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation labels Aug 25, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 25, 2026 21:21
@os-zhuang
os-zhuang added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit fd332ddAug 25, 2026
29 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12242-hero-png-weight branch August 25, 2026 21:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs site: homepage hero cover is a 406 KB PNG served at ~1024 px

1 participant

@os-zhuang