Skip to content

perf: lazy-load HeroBackground (off mobile critical path)#94

Merged
smaramwbc merged 1 commit into
mainfrom
perf-lazy-hero-background
May 25, 2026
Merged

perf: lazy-load HeroBackground (off mobile critical path)#94
smaramwbc merged 1 commit into
mainfrom
perf-lazy-hero-background

Conversation

@smaramwbc
Copy link
Copy Markdown
Owner

Summary

`HeroBackground` is a ~1100-line canvas-based decorative component used only on the homepage. It already self-suppresses on viewports ≤ 639 px (see `useIsHeroCanvasSuppressed` inside the component), so on every mobile visit the entry bundle was downloading, parsing, and evaluating ~17 KB raw of code purely so the component could render a `null`.

Mobile Lighthouse on prod (just measured) showed:

  • Performance 93/100
  • LCP 2.6 s (0.1 s above the "good" threshold)
  • LCP element is the hero `

    ` — confirmed by an existing in-source comment at `src/pages/HomePage.tsx:156`

  • TBT 10 ms, render-blocking resources zero, font-display issues zero
  • LCP critical path = HTML download → entry bundle download → parse → React mount → h1 paint
  • 333 ms of that is `index-*.js` bootup time

So the highest-leverage lowest-risk lever is shrinking the entry bundle by removing eager-loaded code that mobile never uses. HeroBackground fits exactly: decorative, mobile-suppressed, optional on desktop.

Change

  • Convert the `HeroBackground` import in `HomePage.tsx` to `React.lazy` — it gets its own chunk now.
  • Gate the `` mount inside `HeroSection` on a fresh `matchMedia('(max-width: 639px)')` check (with a resize listener for parity with the original in-component suppression hook). Mobile never fetches the chunk at all; desktop fetches it after the entry parses, and the canvas appears a few hundred ms after first paint — invisible for a decorative background layer.
  • Internal `useIsHeroCanvasSuppressed` left in place inside `HeroBackground` as defense-in-depth (in case the component is ever used elsewhere); the gate at HomePage is the load-side optimization.

Bundle effect (`npm run build`)

before after delta
Entry `index-*.js` (raw) 487 KB 470 KB -17 KB
Entry `index-*.js` (gz) 144 KB 138 KB -6 KB
Shared `seo-*.js` (gz) 20 KB 17 KB -3 KB
New `HeroBackground-*.js` (gz) 6 KB desktop only

Net mobile critical-path savings: ~9 KB gz off the entry-and-shared chunks; 17 KB raw never downloaded on mobile.

What I'm NOT doing here

The user explicitly ruled out SSR / Vike for this iteration. This is the smallest-safe-fix variant. If the post-merge re-measurement shows LCP didn't drop below 2.5 s, the next options are (a) further code-splitting of the 122 KB `Heading` chunk (which is really the framer-motion-bearing shared chunk, despite its name), or (b) static prerendering of the homepage hero.

Test plan

  • `npm run build` succeeds; entry chunk visibly shrinks; new HeroBackground chunk appears
  • `npx vitest run` — 276/276 pass locally
  • CI green
  • Vercel preview deploys; homepage looks identical on mobile + desktop; hero canvas appears on desktop with no visible delay
  • After merge + production deploy: re-run mobile Lighthouse against `https://www.statewave.ai/\` and confirm LCP drop

HeroBackground is a ~1100-line canvas-based decorative component used
only on the homepage. It already self-suppresses on viewports ≤ 639px
(useIsHeroCanvasSuppressed), so on every mobile visit the entry bundle
was downloading, parsing, and evaluating the component just for its
render to be a no-op.

Mobile Lighthouse on prod showed LCP = 2.6s (h1 hero headline, per the
existing in-source comment), held back by the entry bundle's parse +
execute cost. Removing HeroBackground from that bundle is the lowest-
risk lever available — it's strictly decorative and already invisible
on mobile.

Change:

* Convert the HeroBackground import to React.lazy, so it gets its own
  chunk (~17 KB raw / ~6 KB gz, plus ~3 KB gz that fell out of the
  shared seo chunk).
* Gate the <HeroBackground /> mount inside HeroSection on a fresh
  matchMedia('(max-width: 639px)') check (with a resize listener for
  parity with the original suppression hook). Mobile never fetches
  the chunk at all; desktop fetches it after the entry parses and
  the canvas appears a few hundred ms after first paint — invisible
  for a background.

Bundle effect (vite build):

  before: entry 487 KB raw / 144 KB gz
  after:  entry 470 KB raw / 138 KB gz   (-17 KB raw / -6 KB gz)
          + HeroBackground 17 KB raw / 6 KB gz (desktop only)

Tests: 276/276 green.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 25, 2026

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

Project Deployment Actions Updated (UTC)
statewave-web Ready Ready Preview, Comment May 25, 2026 10:33am

Request Review

@smaramwbc smaramwbc merged commit b15a6b8 into main May 25, 2026
6 checks passed
@smaramwbc smaramwbc deleted the perf-lazy-hero-background branch May 25, 2026 10:34
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