Skip to content

perf: faster mobile LCP on the landing page#2466

Merged
aalemayhu merged 1 commit into
mainfrom
perf/landing-lcp
May 19, 2026
Merged

perf: faster mobile LCP on the landing page#2466
aalemayhu merged 1 commit into
mainfrom
perf/landing-lcp

Conversation

@aalemayhu
Copy link
Copy Markdown
Contributor

@aalemayhu aalemayhu commented May 19, 2026

Summary

Targets the PageSpeed Insights mobile failure (LCP 3.2 s field / 6.9 s lab, CWV not passing). Four small, independently-shippable fixes:

  • Drop the orphaned /bulma-switch.css <link>. The file does not exist in web/public/ and is not imported anywhere in source — yet the browser still blocked paint waiting for the 404 response. Sneaky win.
  • Make Google Fonts non-blocking via the standard rel="preload" + onload swap, with a <noscript> fallback. font-display: swap is already set, so the fallback font shows immediately and Inter swaps in when it lands.
  • Defer Hotjar + GA until the load event. Both inline IIFEs attached an async script during head parse — small in bytes, but they contributed to the four long main-thread tasks Lighthouse flagged. Session-replay completeness drops by ~1 s on first paint; we keep all the data, just later.
  • Navbar logo (the actual LCP element on /) gets explicit width/height and fetchpriority=\"high\". The dark-theme logo is a separate problem — it's currently a 780×661 mascot PNG scaled to ~33×28, which is wasteful and probably ugly. Flagging for the designer; this PR just reserves the layout space.

Server side: serve /assets/* (hashed Vite output, content-addressed) with Cache-Control: max-age=1y, immutable so repeat visits stop re-validating every chunk.

Why this set, why this order

Mobile field LCP is the actual user-felt problem — students on Android landing here from search rank pressure. Per CrUX, the metric is failing. The four fixes above hit the top three Lighthouse opportunities (render-blocking 600 ms, cache lifetime 98 KiB, image delivery / LCP candidate). Unused-JS (204 KiB) and the deeper bundle splitting are deliberately out of scope — that's a separate, larger PR.

Test plan

  • pnpm tsc --noEmit (server)
  • pnpm --filter 2anki-web typecheck
  • pnpm --filter 2anki-web build (5.7s)
  • pnpm --filter 2anki-web lint (Biome clean)
  • pnpm --filter 2anki-web test --run (101 files / 774 tests pass)
  • Manual smoke on mobile and desktop: landing page paints; theme switcher still works; Hotjar + GA still fire after load
  • After merge: re-run PageSpeed Insights against https://2anki.net/ and confirm mobile LCP drops
  • Confirm Hotjar dashboard still receives sessions (deferred init means events fire post-load, but should still capture interaction)

Follow-ups (separate PRs)

  1. Dark-theme navbar logo. The current dark logo is a 780×661 PNG of the full mascot, displayed at ~33×28 in the navbar. Designer should produce a proper navbar-sized variant matching navbar-logo.png's 140×38 aspect.
  2. Unused JS (204 KiB). Lazy-load HomePage/UploadPage in App.tsx, defer Bugsnag init via requestIdleCallback, lazy-load Google Picker / Dropbox Chooser SDKs on first interaction. Needs a bundle analyzer pass first.
  3. Extend landingPrerender plugin to /. The plugin in web/vite.config.ts already statically prerenders SEO routes (Notion, marketplace, answers). Extending the same pattern to / would put the LCP element in HTML at first byte — much bigger LCP move than full Express SSR, much smaller engineering lift. (See SSR notes in the chat — full SSR is not recommended here.)

Changelog

The landing page paints faster on phones over slow connections — sentence case, no period, no fake warmth, per VOICE.md.

🤖 Generated with Claude Code


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Four narrow fixes targeting the PageSpeed mobile-field LCP failure (3.2 s):

- Drop the orphaned /bulma-switch.css <link>. The file does not exist in
  web/public/ and is unreferenced in source — but the browser still blocked
  paint waiting for the 404. Single biggest render-blocking win.
- Make the Google Fonts stylesheet non-blocking via the standard
  preload + onload swap, with a <noscript> fallback. font-display: swap is
  already set so the fallback font shows immediately.
- Defer Hotjar and Google Analytics inline IIFEs until the load event.
  Both attached an async <script> during head parse — that contributed to
  the long main-thread tasks Lighthouse flagged.
- Add explicit width/height + fetchpriority="high" to the navbar logo
  (the LCP candidate on /). Width differs per theme because the two
  source PNGs have very different aspect ratios — a follow-up should
  give the dark theme a properly sized navbar logo.

Server side: serve /assets/* (hashed Vite output, content-addressed) with
Cache-Control: max-age=1y, immutable. Repeat visits stop re-validating
every chunk.

Changelog entry added (user-visible — first paint is faster on mobile).

Verified: server tsc, web tsc, web build (5.7s), Biome lint, 774 tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented May 19, 2026

Deploy Preview for notion2anki ready!

Name Link
🔨 Latest commit 9e56495
🔍 Latest deploy log https://app.netlify.com/projects/notion2anki/deploys/6a0ca1edf360e50008d357ee
😎 Deploy Preview https://deploy-preview-2466--notion2anki.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@sonarqubecloud
Copy link
Copy Markdown

@aalemayhu aalemayhu marked this pull request as ready for review May 19, 2026 19:42
@aalemayhu aalemayhu merged commit e221b42 into main May 19, 2026
15 checks passed
@aalemayhu aalemayhu deleted the perf/landing-lcp branch May 19, 2026 19:42
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.

1 participant