[perf] Fix mobile LCP, accessibility and dead code from PageSpeed report - #78
Merged
Merged
Conversation
Mobile PageSpeed scored Performance 70 / Accessibility 86 / SEO 92. The performance score decomposed as FCP 10/10 + LCP 0/25 + TBT 30/30 + CLS 25/25 + SI 5/10, so the entire gap was LCP (15.8 s) and Speed Index (5.6 s) — and both traced to one file. LCP was the hero wallpaper div: wallpaper.png is 2,560,981 B at 6144x3456, painted into a ~412 px viewport at 40% opacity, and made up 83% of the 3,101 KiB page. As a CSS background it could not be optimised, could not carry fetchpriority, and was not discoverable until CSS parsed. Images - Enable the optimizer: drop images.unoptimized, migrate the deprecated `domains` key to `remotePatterns`, add AVIF/WebP and a 31-day minimumCacheTTL (the source is ~21 MP, so transforms should be rare). - Convert all four wallpaper CSS backgrounds to <Image fill>; `object-cover` reproduces `bg-cover` geometry exactly, so ScrollLogo's hex-alignment math is unaffected. - Add explicit fetchPriority="high" on each hero — `priority` alone emits the preload link but not the attribute Lighthouse's LCP-discovery audit checks. - Add the missing `sizes` to the event banner. Result: LCP resource 2,560,981 B -> 4,420 B AVIF; total transfer 3,101 KiB -> 203 KiB. R2 banners also optimize (148,990 -> 17,528 B). Accessibility - Mobile menu button had no accessible name: add aria-label (new nav.openMenu / nav.closeMenu messages), aria-expanded and aria-controls. - Footer copyright was #6b7280 on #080814 = 4.11:1, under the 4.5:1 floor; move to text-gray-400 (~8.9:1). - <Button><Link/></Button> rendered <a> inside <button> — invalid HTML, and the anchor was the real tap target at only 20 px tall. Use the asChild slot the component already supports; the three flagged CTAs are now 44 px. Other - Add a browserslist key to drop ~12 KiB of Array/Object/String polyfills. - Delete 45 of 47 components/ui files (only button and table are reachable) plus the orphaned use-mobile hook, and 40 unused dependencies. Tailwind scans components/**, so these fed the 88%-unused render-blocking stylesheet. - Add setRequestLocale so locale routes prerender (130 pages now SSG instead of all dynamic). Note HTML responses still carry no-store because the next-intl middleware sets NEXT_LOCALE on every response, so this is a server-compute win, not a CDN-caching one. Verified: typecheck, lint, format:check, spell, 1018 tests, production build, plus browser checks that the hero renders, ScrollLogo still lands in the hexagon, the mobile menu toggles, and the sr-script cookie still flips /sr to Latin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
setRequestLocale made every locale route prerender, but the pages split events into upcoming/past with today's date — so the split froze at build time, and deploys only happen on merges, which can be weeks apart. An event would keep showing as upcoming days after it happened, and its JSON-LD would keep advertising an InStock offer that buildOffers is supposed to drop for past events. Hourly ISR keeps the static serving (and the LCP win) while letting the date-dependent bits refresh in the background.
dj013
approved these changes
Aug 19, 2026
This branch was successfully deployed
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.
Acts on the mobile PageSpeed report for
cppserbia.org(report): Performance 70 / Accessibility 86 / Best Practices 100 / SEO 92.The performance score decomposed as
FCP 10/10 + LCP 0/25 + TBT 30/30 + CLS 25/25 + SI 5/10— TBT and CLS were already perfect, so the entire 30-point gap was LCP (15.8 s) and Speed Index (5.6 s). Both traced to a single file.The LCP element was the hero wallpaper div.
wallpaper.pngis 2,560,981 B at 6144×3456, painted into a ~412 px viewport at 40 % opacity — 83 % of the 3,101 KiB page. As a CSS background it couldn't be optimised, couldn't carryfetchpriority, and wasn't discoverable until CSS parsed.Measured effect
<a>nested in<button>Changes
Images — enabled the optimizer in
next.config.mjs(droppedunoptimized, migrated the deprecateddomainskey toremotePatterns, added AVIF/WebP and a 31-dayminimumCacheTTLsince the source is ~21 MP). Converted all four wallpaper CSS backgrounds to<Image fill>, added the missingsizesto the event banner, and setfetchPriority="high"on each hero —priorityalone emits the preload link but not the attribute Lighthouse's LCP-discovery audit actually checks (priorityHinted: falsewas the failing item).object-coverreproducesbg-covergeometry exactly, soScrollLogo's hex-alignment math is untouched.Accessibility — three concrete failures:
aria-label(newnav.openMenu/nav.closeMenumessages in both locales), plusaria-expandedandaria-controls.#6b7280on#080814= 4.11:1, under the 4.5:1 floor →text-gray-400(~8.9:1).<Button><Link/></Button>rendered<a>inside<button>— invalid HTML, and the anchor was the real tap target at only 20 px tall. Switched to theasChildslot the component already supported (and whichadd-to-calendar-button.tsxalready used).Other — added a
browserslistkey to drop ~12 KiB ofArray/Object/Stringpolyfills; deleted 45 of 47components/uifiles (onlybuttonandtableare reachable from any page) plus the orphaneduse-mobilehook and 40 unused dependencies — Tailwind scanscomponents/**, so these fed the 88 %-unused render-blocking stylesheet; addedsetRequestLocaleso locale routes prerender.Notes for the reviewer
setRequestLocaleonly half-delivers its goal. Pages now prerender at build time, but HTML responses still carryCache-Control: no-storebecause the next-intl middleware setsNEXT_LOCALEon every response. I testedlocaleDetection: false— it does not fix this — so this is a server-compute win, not a CDN-caching one. Removing it would mean redesigning the i18n middleware; out of scope here.www.cppserbia.orgbut everybaseUrlin the code says the apex, sorel=canonicalpoints at a redirecting host. Flipping the primary domain tocppserbia.orgfixes it with no code change.lockfileVersion: '9.0'and remains readable by CI's pinned pnpm 9.email-decode.min.js(451 ms in the report). That's the Scrape Shield → Email Address Obfuscation toggle, not a code change.Verification
typecheck,lint,format:check,spell, 1018 tests, and a production build all pass. Browser-checked at 412×915 that the hero renders,ScrollLogostill lands in the hexagon while scrolling, the mobile menu toggles, all CTAs navigate, and thesr-scriptcookie still flips/srto Latin. The cold/_next/imagetransform of the 21 MP source returns 200 in ~1.3 s (no timeout), and R2 event banners optimize too (148,990 → 17,528 B).