Skip to content

perf(landing): stop eager 2.7MB video + 581KB codec on mobile landing#296

Closed
ignromanov wants to merge 2 commits into
developfrom
perf/landing-video-codec
Closed

perf(landing): stop eager 2.7MB video + 581KB codec on mobile landing#296
ignromanov wants to merge 2 commits into
developfrom
perf/landing-video-codec

Conversation

@ignromanov
Copy link
Copy Markdown
Owner

What

Landing-page performance fix — two regressions/inefficiencies on the mobile landing path.

  • P0 — mobile VideoSection no longer autoplays. autoPlay was overriding preload="none" (HTML spec), forcing eager download of the 2.7MB voidpay-9x16-v2.mp4 on the critical path. Now: poster + tap-to-play via native controls; preload="none" actually defers the fetch. Desktop autoplay + reduced-motion paths preserved.
  • P1 — lifted getDemoInvoices() (runs encodeInvoice → brotli-wasm) out of a client useEffect to the landing RSC. page.tsx is now async; resolved DemoInvoice[] is threaded as a prop down LandingContent → BelowFoldSections → DemoSection. The client no longer imports the codec → 581KB brotli-wasm no longer loads on the landing (still loads on /create + /pay).

Why — measured (Lighthouse mobile)

Metric prod (live) develop (live) this branch (preview, warm median)
Score 68 67 84 (range 73–93)
Total byte-weight 1,222 KiB 4,044 KiB 376 KiB (−69% vs prod)
TBT 950 ms 770 ms 140 ms (−85%)
Speed Index 4.2 s 5.0 s 3.8 s
FCP 1.1 s 1.2 s 1.2 s (no regression)
LCP 3.5 s 3.7 s 3.7 s (range 2.5–3.8; no regression)
CLS 0 0 0
Media (video) / wasm (codec) reqs 1 / 1 0 / 0

The develop byte-weight spike (4,044 KiB) was the 2.7MB autoplay MP4; P0 removes it from the initial trace. The first cold-preview sample showed FCP 2.6/LCP 4.3 — a cold-lambda artifact; warm median matches/beats baseline.

Verification

Iris quality-gate: PASS — type/lint/build clean, 2831 tests (2 skipped), build compiles landing as Static ○ (confirms async-RSC + prop boundary), acceptance 13/13 (P0 + P1), FSD clean, RSC→client prop boundary serializable.

Follow-up (P2, non-blocking)

  • VideoSection coverage ~35% — structurally capped by the global useReducedMotion=true test mock (observer/desktop-autoplay paths unreachable). Tracked separately.

🤖 Generated with Claude Code

On mobile, autoPlay was set to !prefersReducedMotion which overrides
preload="none" per HTML spec, causing the full 2.7MB MP4 to download
on the critical path. Gate autoPlay on !isMobile && !prefersReducedMotion
so the deferred fetch actually defers on mobile.

- Show poster with native controls on mobile (tap-to-play)
- Skip IntersectionObserver autoplay resume on mobile
- Desktop behavior unchanged (autoplay when not reduced-motion)
- Add tests: mobile no-autoplay, mobile controls present
… on landing (P1)

DemoSection was calling getDemoInvoices() in a useEffect, triggering a
581KB brotli-wasm fetch on every landing page visit. Lift the async call
to the server boundary (LandingPage RSC) so encodeInvoice runs in Node
at request time. Pass resolved DemoInvoice[] as a prop through
LandingContent → LazyBelowFold → BelowFoldSections → DemoSection.

- DemoSection: remove useEffect fetch, accept demoInvoices prop
- demo-invoices.ts: export DemoInvoice type, fix stale "build time" comment
- BelowFoldSections: thread demoInvoices prop through
- LandingContent: accept + pass demoInvoices prop
- page.tsx: async RSC — await getDemoInvoices() before render
- Tests: prop-driven fixture via getDemoInvoices() in beforeAll (mocked codec)
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 5, 2026

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

Project Deployment Actions Updated (UTC)
voidpay Ready Ready Preview, Comment Jun 5, 2026 5:14am

@ignromanov
Copy link
Copy Markdown
Owner Author

HOLD — split. Official PSI (mobile, 3× median) shows the P1 RSC lift introduced an LCP +1.4s / SI +1.8s regression (demo-invoice render pulled into the critical path) despite the payload/TBT wins. P0 (video) extracted to a clean PR. P1 (codec-off-landing) being reworked: precompute demo hashes at build + keep DemoSection lazy client-side below the fold. This PR will be closed once P0 + reworked-P1 land.

ignromanov added a commit that referenced this pull request Jun 5, 2026
…m client (#300)

DemoSection no longer loads brotli-wasm or @/features/invoice-codec at runtime.
createHash strings are generated once during `pnpm build` (prebuild npm hook) via
scripts/generate-demo-hashes.ts + a Node zlib brotli stub that replaces brotli-wasm
for the build-time script. DemoSection imports static data + the generated
demo-invoices.generated.ts module — synchronous, no useEffect async loading.

This reverts the #296 RSC lift (which regressed LCP +1.4s / SI +1.8s) while
still keeping brotli-wasm off the landing client bundle.

Files:
- scripts/generate-demo-hashes.ts — prebuild script that writes createHash strings
- scripts/brotli-node-stub.ts — Node zlib brotli shim for the prebuild tsconfig
- scripts/tsconfig.prebuild.json — tsconfig alias: brotli-wasm → brotli-node-stub
- src/widgets/landing/constants/demo-invoices.ts — exports RAW_DEMO_INVOICES + buildDemoInvoices()
- src/widgets/landing/constants/demo-invoices.generated.ts — placeholder (overwritten at build)
- src/widgets/landing/demo-section/DemoSection.tsx — imports static data, no codec
- src/widgets/landing/demo-section/__tests__/DemoSection.test.tsx — updated for static shape
@ignromanov
Copy link
Copy Markdown
Owner Author

Superseded. Split into #299 (P0 video — merged) + #300 (reworked P1 codec-off-landing via build-precompute — merged). This version's RSC-lift caused an LCP regression (5.4s vs develop 4.1s on official PSI mobile median); #300 achieves the same codec-off-landing goal without the regression (4.2s). Closing in favor of the two merged PRs.

@ignromanov ignromanov closed this Jun 5, 2026
@ignromanov ignromanov deleted the perf/landing-video-codec branch June 5, 2026 18:00
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