Uh oh!
There was an error while loading. Please reload this page.
perf(docs): re-encode the hero cover to WebP and hoist its path into one constant - #12377
Merged
Conversation
…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>
3 tasks
os-zhuang
marked this pull request as ready for review
August 25, 2026 21:21
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 startin this worktree (port 3242), not fromlsand 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./hero-cover-dark.png/hero-cover-dark.webpContent-Typeimage/pngimage/webpContent-Length/curlsize_downloadContent-EncodingtransferSizeThe 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:
max-w-5xl(64rem) minuspx-6padding 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:heightare 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:
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 theog: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_COVERnow lives inapps/docs/lib/site.tsbesideSITE_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:app/[lang]/page.tsx— videoposterposter="/hero-cover-dark.png"poster={HERO_COVER.url}app/[lang]/page.tsx—HOME_CARD(OG + Twitter + JSON-LDimage)HERO_COVER(constant deleted)app/[lang]/blog/[[...slug]]/page.tsx—BLOG_CARD(index + every post, OG + Twitter +BlogPostingimage)HERO_COVER(constant deleted)Verified in the served HTML rather than in the source:
A repo-wide grep leaves no
/hero-cover-dark.pngURL anywhere; the only remaining.pngspellings 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.tsxis outside the dispatched file surface. One clause of its docblock said the cover isdocs/screenshots/hero-cover-dark.png"copied intopublic/" — 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 atHERO_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 resolvesUntouched, and proved rather than asserted:
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:pnpm --filter @objectstack/docs typecheckVERDICT command-exit 0next build(apps/docs)✓ Compiled successfully,VERDICT command-exit 0pnpm check:docs-locale-catch-allVERDICT command-exit 0pnpm check:page-declaration-shapeVERDICT command-exit 0pnpm check:published-filesVERDICT command-exit 0pnpm check:test-source-aliasVERDICT command-exit 0pnpm check:type-source-resolutionVERDICT command-exit 0pnpm check:nul-bytesVERDICT command-exit 0The five
check:*families are whatnode scripts/pm/dispatch-gates.mjsderives for this diff's six paths (it reads the change set from git itself);check:nul-bytesis owed by any edit.Verification ran UNLOCKED, declared.
scripts/pm/os-verify-lock.shreportsno 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 endsVERDICT … · UNLOCKED (declared).Repo-wide
pnpm lintis 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:ESLint#isPathIgnoredsays all four changed source files are in scope and both changed binaries (.png,.webp) are ignored.--format json: exactly 4 files reported linted, 0 errors, 0 warnings, exit 0 — so nothing was silently skipped.calculateConfigForFilereturnsparserOptions.project: null,projectService: null,EXPERIMENTAL_useProjectService: nullfor every probed file, inside and outsideapps/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/docsisprivate: trueand this publishes nothing.skip-changesetapplied.Generated by Claude Code