Skip to content

[perf] Fix mobile LCP, accessibility and dead code from PageSpeed report - #78

Merged
duxi90 merged 2 commits into
mainfrom
perf/pagespeed-mobile
Aug 19, 2026
Merged

duxi90 merged 2 commits into
mainfrom
perf/pagespeed-mobile

Conversation

@duxi90

@duxi90 duxi90 commented Aug 13, 2026

Copy link
Copy Markdown
Member

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.png is 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 carry fetchpriority, and wasn't discoverable until CSS parsed.

Measured effect

Before After
LCP resource 2,560,981 B PNG 4,420 B AVIF
Total page transfer 3,101 KiB 203 KiB
<a> nested in <button> 5 0
Smallest flagged tap target 20 px tall 44 px
Locale routes all dynamic 130 prerendered (SSG)
Dependencies 61 22

Changes

Images — enabled the optimizer in next.config.mjs (dropped unoptimized, migrated the deprecated domains key to remotePatterns, added AVIF/WebP and a 31-day minimumCacheTTL since the source is ~21 MP). Converted all four wallpaper CSS backgrounds to <Image fill>, added the missing sizes to the event banner, and set fetchPriority="high" on each hero — priority alone emits the preload link but not the attribute Lighthouse's LCP-discovery audit actually checks (priorityHinted: false was the failing item).

object-cover reproduces bg-cover geometry exactly, so ScrollLogo's hex-alignment math is untouched.

Accessibility — three concrete failures:

  • Mobile menu button had no accessible name → aria-label (new nav.openMenu / nav.closeMenu messages in both locales), plus aria-expanded and aria-controls.
  • Footer copyright was #6b7280 on #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 the asChild slot the component already supported (and which add-to-calendar-button.tsx already used).

Other — added a browserslist key to drop ~12 KiB of Array/Object/String polyfills; deleted 45 of 47 components/ui files (only button and table are reachable from any page) plus the orphaned use-mobile hook and 40 unused dependencies — Tailwind scans components/**, so these fed the 88 %-unused render-blocking stylesheet; added setRequestLocale so locale routes prerender.

Notes for the reviewer

  • setRequestLocale only half-delivers its goal. Pages now prerender at build time, but HTML responses still carry Cache-Control: no-store because the next-intl middleware sets NEXT_LOCALE on every response. I tested localeDetection: 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.
  • The SEO 92 → 100 fix is not in this PR. It's a Vercel domain change: the site is served at www.cppserbia.org but every baseUrl in the code says the apex, so rel=canonical points at a redirecting host. Flipping the primary domain to cppserbia.org fixes it with no code change.
  • The lockfile was regenerated with pnpm 10 so it stays lockfileVersion: '9.0' and remains readable by CI's pinned pnpm 9.
  • Cloudflare is injecting a render-blocking 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, ScrollLogo still lands in the hexagon while scrolling, the mobile menu toggles, all CTAs navigate, and the sr-script cookie still flips /sr to Latin. The cold /_next/image transform of the 21 MP source returns 200 in ~1.3 s (no timeout), and R2 event banners optimize too (148,990 → 17,528 B).

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>
@vercel

vercel Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cppserbia-org-website Ready Ready Preview, v0 Aug 19, 2026 11:53am

Request Review

@duxi90
duxi90 requested a review from dj013 August 13, 2026 16:22
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.
@duxi90
duxi90 merged commit 264a6ad into main Aug 19, 2026
4 checks passed
@duxi90
duxi90 deleted the perf/pagespeed-mobile branch August 19, 2026 11:56

This branch was successfully deployed

1 active deployment
Preview — 6d57af71 Deployed Aug 19, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants