perf: homepage Lighthouse fixes (images, LCP, fonts, Zenodo CORS, a11y) - #239
Merged
Merged
Conversation
Lighthouse on the homepage scored 61 with an LCP of 4.5 s, almost all of it image weight: 26 MiB of images for a page that shows them at thumbnail size. - Turn Next's image optimizer back on (`images.unoptimized` was set when the social feed landed) and allow the API media host. Geostory covers are served as WebP at 96/256px instead of the original JPEG: the largest one went from 23.5 MiB to 12 KiB. Fosstodon avatars go from ~190 KiB to ~3 KiB. - Render Fosstodon post pictures through `next/image` instead of as CSS backgrounds, so they are resized to the card as well. - Replace the 1.2 MiB CSS PNG starfield with a preloaded, viewport-sized `next/image` with `fetchPriority="high"`: 57–87 KiB as WebP, discoverable from the HTML, no longer waiting on the stylesheet. - Inter used `display: 'block'`, hiding all text for up to 1.7 s on a throttled run; swap in with the size-adjusted fallback instead. - Preconnect to the API, Fosstodon, Zotero and the Esri tile server, which the page hits right after hydration. - Fetch the geostories list once: the globe page requested it a second time with no params (another 730 KiB) only to look up the selected story. - Drop the direct `sharp` 0.32 dependency; Next 16 ships the 0.34 it needs. Covers and avatars are cached by the optimizer for 30 days; the first request for a cover still pays for the origin download.
Zenodo's API sends no `Access-Control-Allow-Origin`, so every homepage load logged a CORS error and the publications list silently lost its Zenodo records. `/api/zenodo/records` fetches them on the server, forwards only the query parameters the app uses, and caches the answer for 15 minutes so one copy serves every visitor instead of each of them hitting Zenodo's rate-limited API. The e2e stubs now target the proxy path; the Zenodo stub no longer needs a CORS header since it is same-origin.
- Carousel dots were 8px buttons, below the 24px minimum target size. Each disc now sits inside a 24px button; the strip math follows the button size and the active dot exposes `aria-current`. - The live feed author name was an `h4` under a page whose only heading is the `h1`; it is a paragraph now. - Cards with a picture had only the author as link text, so several cards read as identical links. Each card now carries a hidden description of what it opens. - Cesium appends credit widgets whose anchors have no `href`; they were hidden by CSS but still flagged as uncrawlable. They are mounted into detached elements, so they never enter the document. The visible credits are read off `viewer.creditDisplay` as before.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
`NEXT_PUBLIC_API_IMAGES_URL` ends with a slash on Vercel, so covers were requested as `https://api.earthmonitor.org//media/g31.jpg`. The media server tolerated that, but the image optimizer matches the path against `/media/**` and answered 400 for every cover on the preview. Trailing slashes are now stripped before the path is appended. The allowed media host is read from the same env var, so a preview pointed at another media server stays within the optimizer's allowlist.
The 79px covers looked soft once they went through the image optimizer: on a 1x screen they were a 96px WebP at quality 75, where a browser used to downsample the multi-megapixel original. The image is now declared at twice its box, so the optimizer serves 256px on 1x screens and 384px on 2x and the browser downsamples a detailed bitmap, and it is encoded at quality 90. Next 16 only encodes listed qualities, so 90 is added to `images.qualities`. Per cover this is ~4 KiB (1x) to ~7 KiB (2x), against 1.6 KiB before and up to 23 MiB originally.
The live feed panel slides in from the right for 700ms after load. The test read the strip's box, then the buttons' boxes in a second call; while the panel was still moving the two readings disagreed, and a seventh button appeared to intersect the strip. The old 8px dots with 8px gaps usually hid this, the edge-to-edge 24px buttons do not, so the test failed in CI where the page is slower to settle. The test now waits for every finite animation on the page to finish and takes all the rectangles in one call, from the same frame.
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.
Summary
A Lighthouse run on the homepage (
app-dev.earthmonitor.org, 2026-09-07) scored 61 for performance with an LCP of 4.5 s and a Speed Index of 6.1 s. Nearly all of it was image weight: 26 MiB of images for a page that shows them at thumbnail size. This PR works through the report's flagged items.Performance
images.unoptimized: truehad been set when the social feed landed. Geostory covers are now served as WebP at 96/256px instead of the original JPEG (the largest went from 23.5 MiB → 12 KiB); Fosstodon avatars ~190 KiB → ~3 KiB. Optimized variants are cached 30 days.next/imagewithfetchPriority="high": 57–87 KiB as WebP.next/imageinstead of CSS backgrounds, so they are resized to the card.display: 'block'(text hidden up to 1.7 s on a throttled run) →swap.sharp0.32 dependency; Next 16 ships the 0.34 its optimizer needs.Fixed
Access-Control-Allow-Origin, so the publications list silently lost its Zenodo records. New/api/zenodo/recordsroute handler fetches server-side, forwards only allowlisted query params, caches 15 min.aria-currentadded); author name is no longer anh4under anh1-only page; image-only feed cards carry a hidden description so they no longer read as identical links.href) are mounted into detached elements so they never enter the document.Not addressed (for awareness)
api.earthmonitor.orgserves over HTTP/1.1 and the geostories JSON is 733 KiB per call — backend items.Test plan
yarn check-types,yarn lint,yarn build/_next/imagefor a 23.5 MiB cover returns 12 KiB WebP; bg returns 57–87 KiB WebP;<img>and preload carryfetchPriority="high"; preconnect tags present;/api/zenodo/recordsreturns 3 hits withs-maxage=900/_next/image(standalone output traces Next's nestedsharp)