Context
A content-heavy product where Largest Contentful Paint and Interaction to Next Paint were failing on real-user metrics, despite a respectable lab Lighthouse score. The gap between lab and field meant the issue was in real network and device conditions, not idealised builds.
Constraints
Couldn't drop the analytics or third-party scripts business needed. Couldn't fork to a different framework. Existing CDN setup had to keep working. Performance budget had to be enforceable, not aspirational.
Decisions
Shift focus from Lighthouse to field RUM. Treat CWV as a budget, not a goal. Address the dominant LCP element first (above-the-fold image), then route-level JS, then third-party scripts. Build a CI check that fails when bundle size or known LCP element regresses.
Implementation
Above-the-fold images served as AVIF with explicit dimensions and `fetchpriority=high`. Route-level code splitting with `loadComponent` + targeted prefetch on intent. Third-party scripts moved behind interaction or visibility triggers. Bundle budget gating in CI.
Outcome
LCP and INP moved into the green band on real-user data. Performance stopped being a recurring fire and became a property guarded by CI. The team gained a vocabulary for tradeoffs ("this costs us 30 KB on the LCP route").
Outcome metrics
- Performance
- LCP/INP green on field data
- Maintainability
- Performance guarded by automated checks
- Bundle size
- Per-route budgets enforced in CI