web.dev: Web Vitals - discussion (2023-03-18)
The web vitals page is great because it forces a simple question: what does "fast" mean in a way users can feel and teams can measure? I'm curious how people are translating vitals into React work items without falling into placebo optimizations.
Do you set budgets per route/surface, or do you enforce a single global bar? Do you log/record performance evidence (LCP/INP/CLS) alongside route posture so you can correlate regressions with UI state? What changes actually moved the needle for you in React apps: memoization, routing changes, data strategy, or something else?
Comments (14)
Back to latestBudgets per route.
A dashboard app and a marketing page don't need the same bar.
We started printing performance evidence as a tips line in dev and it changed behavior immediately:
txt
[tips] perf route=/feed lcp=2100 inp=120 cls=0.03 evidence=perf:ok
[tips] perf route=/feed lcp=3200 inp=280 cls=0.08 evidence=perf:regress
Even if the numbers aren't perfect, the trend becomes visible.
Docs tie-in: a lot of vitals issues end up being rendering posture and route-enter work.
Related: Performance and Rendering and Routing and Navigation.
Long-form: budgets only work when the app's state is observable.
If you see LCP regress, you need to know what lane you were in (pending vs ok), whether you were rendering cached data, and what route posture was active. Otherwise you're just guessing at fixes.
We treat performance metrics as part of the contract narrative: the app should be able to say "I was pending because of X" and "I rendered ok at time Y".
Concrete alternative: don't chase metrics, chase user complaints.
I get it, but metrics catch regressions before complaints. The trick is using metrics as smoke alarms, not as the only goal.
Exactly. We treat vitals as regression detection.
When a metric moves, we ask what contract surface changed (route enter work, caching lane, layout stability).
The biggest win for us was reducing route-enter work and deferring non-critical computations.
Memoization did almost nothing compared to changing what we do on route enter.
We fixed CLS by making loading fallbacks stable. A lot of our shift came from placeholders that didn't match the final layout.
Once we treated fallbacks as a contract, the layout stopped moving.
Short take: performance work is mostly about deciding what to not do on initial render.
We found INP improved when we stopped doing sync derived work on every keystroke.
We introduced a draft/commit split and logged commit transitions. INP dropped because we stopped blocking the main thread with helpful recomputation.
Long-form counterpoint: it's easy to over-index on global metrics and miss per-feature pain.
We had a route that was fine globally but had a horrible worst-case path. Budgets per route fixed that because it forced us to acknowledge the outliers.
We started capturing a screenshot + evidence + perf numbers together in CI for key routes.
It made performance regressions as actionable as functional regressions.
If your caching strategy is inconsistent, your vitals will be inconsistent.
We stabilized metrics by making request identity and cache lane explicit.
A practical approach: pick one metric to defend per quarter and tie it to a route budget.
If you try to defend everything always, you'll stop doing product work.
Takeaway: vitals become usable when you integrate them into the contract narrative (route + lane + source).
Otherwise it's just numbers and vibes.