perf: serve images at the size the page actually shows - #43
Merged
Conversation
`output: "export"` forces `images: { unoptimized: true }`, so next/image does
no resizing and the committed file is exactly what every visitor downloads.
Nothing in the intake resized anything, so headshots went up straight from a
phone: team/alex.jpg was 2644x2644 and 1,096 KB to fill a 48px circle, and
/team weighed 8.5 MB.
Resize every committed image to its display size in one pass from the
originals - 95 images, 16.01 MB to 4.57 MB, taking /team to 2.1 MB. Formats
and filenames are unchanged on purpose: renaming an asset means editing every
reference in src/content, and a missed one fails silently to initials rather
than to a visible broken image. Nearly all of the win is the resize anyway.
Add scripts/optimize-images.mjs to do it, and check:images to CI so the next
oversized headshot fails the pull request instead of shipping. The script only
touches files that break their budget, so it never re-compresses a compliant
file a second generation, and it applies EXIF orientation before stripping
metadata - which also drops any GPS coordinates the camera recorded.
Note for anyone editing the encoder: png({ effort }) and png({ palette }) put
libvips into its palette path, which silently drops the alpha channel. That
turned coinbase.png opaque, which would have rendered as a dark rectangle on
the white logo tile while passing every automated check. It now encodes with
compressionLevel only and verifies transparency survived.
Full-resolution originals remain in git history at 32e1ab9.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Picks up the resized images from the previous commit. No HTML, JS or CSS changed - the filenames are the same, only the bytes behind them. The __next.* prefetch payloads are left as they are. Next writes those with a platform-dependent path shape, so regenerating on Windows would delete the Linux-shaped files and add Windows-shaped ones without changing any content. CI already excludes them from its drift check for the same reason (UPD-004). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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 free
to 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.
What
Images were committed at their original camera resolution and served unchanged.
output: "export"requiresimages: { unoptimized: true }— GitHub Pages has no image optimization server — sonext/imagedoes no resizing, emits nosrcset, and does no format negotiation. The committed file is exactly what every visitor downloads.Nothing in the intake resized anything, so headshots went up straight from a phone.
team/alex.jpgwas 2644×2644 / 1,096 KB to fill a 48px circle, and/teamweighed 8.5 MB.95 images, 16.01 MB → 4.57 MB (−71%).
/team/demos/media//team/<person>This matters more here than on most hosts: GitHub Pages serves everything with
Cache-Control: max-age=600and will not allow custom headers, so a returning visitor re-pays the full weight every ten minutes. Sending fewer bytes is the only available lever.Analysis and method:
docs/PERFORMANCE.md.How
scripts/optimize-images.mjsresizes in place against a per-directory budget (avatars 384px, logos 256px, posters 600px, summit 1200px, backgrounds/heroes 1600px), with per-file overrides.npm run check:imagesruns in CI, so the next oversized headshot fails the pull request instead of shipping.CONTRIBUTING.mdandpublic/team/README.md.sharppromoted from a transitivenextdependency to an explicitdevDependency.Reviewer notes
Formats and filenames are unchanged on purpose. Converting to WebP would have saved another ~10%, but renaming an asset means editing every reference in
src/content, and a missed one fails silently —TeamAvatarfalls back to initials rather than showing a broken image. Nearly all of the win is the resize.A silent corruption bug this nearly shipped. sharp's
png({ effort })andpng({ palette })put libvips into its palette-quantizing path, which drops the alpha channel on some images. It hit four logos.coinbase.pngis mostly transparent, so it would have rendered as a solid dark rectangle on the white logo tile — while passing the build, the byte budget, and every automated check. The encoder now usescompressionLevelonly and verifies transparency survived. Worth knowing before anyone tunes the encoder for a few more KB.Lossy and irreversible. Every image was re-encoded in a single pass from the originals, so there is no generational loss. Full-resolution originals remain in git history at
32e1ab9f.Two commits, split per CONTRIBUTING §4 — source first, then the generated
static-site/snapshot, so the real diff stays reviewable.The
__next.*prefetch payloads are deliberately untouched in the snapshot commit. Next writes those with a platform-dependent path shape, so regenerating on Windows would delete 140 Linux-shaped files and add 75 Windows-shaped ones with no content change. CI excludes them from its drift check for the same reason (UPD-004).Verification
/team, a profile page,/demos, and the partner logo band in both dark and light themes.Also: EXIF is stripped after orientation is applied, which removes any GPS coordinates the cameras recorded on the published headshots.
Not included
Responsive
srcset, lower book-render scale, lazy book pages, and removing/font-labare tracked as UPD-015 and UPD-016. The font lab is recent in-flight work, so removing it is its author's call.