How do you fix poor Web Vitals in React via profiling and CI?
answer
I start with a network waterfall to spot critical LCP assets and third-party blockers, then profile main-thread long tasks (CPU, parse/compile, layout) and measure hydration cost with React Profiler. Fixes: right script strategy (defer/async, priority hints, code splitting), preload fonts and hero media, and build a strict image pipeline (responsive AVIF/WebP, lazy, placeholders). With React concurrency I enable streaming SSR, selective hydration, Suspense, and useTransition to keep input responsive.
Long Answer
When Core Web Vitals regress, I treat it like an incident: establish a baseline, isolate bottlenecks, then ship targeted fixes that move LCP, INP/FID, and CLS. My plan has three profiling passes and four concrete remediation tracks.
1) Profiling pass A — Network waterfall
Open DevTools → Performance/Network and run a cold load on a throttled profile. Identify:
- LCP candidate (hero image or first meaningful text) and confirm its request start, TTFB, and response size.
- Render-blocking resources: CSS, large synchronous scripts, third-party tags.
- Priority drift: critical assets queued behind analytics, tag managers, or non-essential JS.
Output: a list of top offenders (bytes, priority, domain), and a dependency map of what blocks first paint and LCP.
2) Profiling pass B — Main-thread analysis
Record a Performance trace and inspect long tasks (>50 ms). Attribute time to:
- Script parse/compile (bundle size, module evaluation).
- Layout/style work and forced reflows (layout thrash).
- React render/hydration stacks that monopolize the thread.
Also check Total Blocking Time (TBT) and interaction responsiveness (INP/FID) to see where user input stalls.
3) Profiling pass C — React hydration cost
Use React Profiler (production build with profiling) to measure:
- Hydration phase duration per route/root.
- Components with heavy mount effects, deep renders, or repeated memo busting.
- Suspense waterfalls (sequential data fetches) that delay paint.
Output: a per-component cost table and candidates for deferring to server or splitting into islands.
Remediation Track 1 — Script strategy and bundling
- Code splitting by route and above-the-fold shards; load only what the first view needs.
- Mark non-critical scripts defer/async; delay third-party (analytics, chat) until idle or user intent.
- Use priority hints (<link rel="preload" as="script">, fetchpriority="high" for the critical module).
- Remove unused polyfills; ship modern builds (ESM) and only targeted legacy where necessary.
- Audit bundle with coverage; eliminate dead code and reduce client-side libraries (favor native APIs, smaller libs).
Remediation Track 2 — Preloading and critical path
- Preload the LCP asset (hero image) and its font; inline critical CSS for the above-the-fold layout.
- Serve fonts with font-display: swap or optional + size-adjust to avoid CLS from FOIT/FOUT.
- Prefetch next-likely route chunks on idle/hover to accelerate secondary navigation.
Remediation Track 3 — Image pipeline
- Serve responsive AVIF/WebP via srcset/sizes; compute intrinsic dimensions to prevent CLS.
- Use priority or fetchpriority="high" for the hero image only; lazy load the rest with low-quality placeholders (LQIP/blurhash).
- Strip metadata, limit DPI, and avoid oversized containers that trigger reflow.
- For carousels and galleries, virtualize off-screen slides and delay heavy decoding.
Remediation Track 4 — React concurrency and hydration
- Enable streaming SSR so HTML arrives progressively; pair with Suspense to stream shells and hydrate critical islands first.
- Use selective/partial hydration (islands via Next.js App Router or similar) to reduce the first render’s JS.
- Move heavy computation to server components or to workers; keep client components lean.
- For input flows, gate expensive renders behind useTransition/useDeferredValue to prioritize interaction.
- Audit effects; convert mount-time work into lazy handlers or server-side precomputation. Memoize where stable.
Measurement and governance
Re-run traces after each change. Track:
- LCP: request ordering and first byte → decode → paint.
- INP/TBT: long task count and duration before/after splitting.
- CLS: shifts from late images, ads, and font swaps.
Automate Lighthouse/Web Vitals in CI with budgets (e.g., LCP < 2.5 s p95, CLS < 0.1, INP < 200 ms p95). Gate merges on regressions and post a diff report per PR. Production RUM validates that lab gains hold across devices and networks.
Bottom line: profile the network, tame the main thread, and right-size hydration. Then institutionalize the wins with budgets so performance stays fixed, not just “fixed once.”
Table
Common Mistakes
- Preloading everything; starving the real LCP asset.
- Shipping one mega-bundle; pushing parse/compile into long tasks.
- Hydrating the entire page eagerly; no islands or streaming.
- Lazy loading above-the-fold images, worsening LCP.
- Letting fonts block text or swap late, causing CLS.
- Overusing Suspense without caching, creating waterfalls.
- Injecting third-party tags in <head> with async but hard dependencies elsewhere.
- Measuring only Lighthouse once; ignoring p95 RUM where users actually struggle.
Sample Answers
Junior:
“I run a Performance trace to find long tasks and check which request is the LCP image. I preload that image, defer non-critical scripts, and lazy load below-the-fold media. I also split bundles so the first route ships less JS.”
Mid:
“I map the waterfall, then use React Profiler to quantify hydration cost. I switch to AVIF/WebP with size attributes to avoid CLS, preload fonts and hero, and move analytics to idle. With Suspense and streaming SSR, I hydrate critical islands first and keep interactions smooth with useTransition.”
Senior:
“I run a three-pass triage (network → main thread → hydration), then apply a four-track fix: script strategy and code splitting, critical preloads, a disciplined image pipeline, and React concurrency (streaming SSR, islands, server components). Changes are gated by CI budgets and verified with RUM at p95.”
Evaluation Criteria
Strong answers show a repeatable profiling plan (network, main thread, hydration) and map each finding to specific fixes: code splitting + defer/async, targeted preloads, responsive images with dimensions, and React concurrency (Streaming SSR, Suspense, selective hydration). They reference measurement in CI and RUM p95, not just lab scores. Red flags: generic “optimize images,” no understanding of hydration cost, preloading indiscriminately, or ignoring long tasks. Bonus: server components, worker offload, next-route prefetch on idle.
Preparation Tips
- Practice DevTools traces; tag the LCP request and list top three long tasks.
- Install React Profiler; capture hydration cost before/after streaming SSR.
- Convert hero media to AVIF/WebP with srcset and correct dimensions; compare LCP.
- Implement route-level code splitting and verify smaller parse/compile in the flamegraph.
- Add fetchpriority="high" to the real hero, not thumbnails.
- Try Suspense + data caching to remove waterfalls.
- Add Lighthouse CI budgets and fail a PR on LCP or INP regression; read the diff.
- Verify results with web-vitals RUM on a slow phone profile.
Real-world Context
A marketplace’s LCP was 4.6 s. The hero image queued behind two tag-manager scripts. Preloading the hero, deferring tags, and splitting the entry cut LCP to 2.2 s. A SaaS app showed 300–500 ms INP spikes from 1.2 MB of JS parse/compile; route-based splitting and moving charts to worker threads halved TBT and stabilized INP. A content site had hydration taking 1.3 s; enabling streaming SSR with Suspense islands rendered above-the-fold HTML quickly and deferred the sidebar, improving interactivity without visual regressions.
Key Takeaways
- Profile in layers: waterfall → main thread → hydration.
- Give the true LCP asset priority; defer everything else.
- Shrink and split JS; fix long tasks before micro-tuning.
- Build a real image pipeline with dimensions and AVIF/WebP.
- Use streaming SSR, Suspense, and selective hydration to stay responsive.
Practice Exercise
Scenario:
Your React storefront fails Web Vitals (LCP 3.8 s, INP 320 ms, CLS 0.18). You must diagnose and fix it in two sprints.
Tasks:
- Record a cold-load trace on “Home” with network throttling. Identify the LCP request, its priority, and blockers. Produce a waterfall sketch with three fixes.
- Profile the main thread. List the top five long tasks and whether they are parse/compile, layout, or React render. Create a plan to eliminate at least 60% of TBT.
- Run React Profiler on the first route. Measure hydration time and name the top three expensive components.
- Apply fixes: route-level code splitting; defer non-critical scripts; preload hero image and first font; convert hero to AVIF with dimensions; add LQIP and lazy load below-fold media.
- Enable streaming SSR with Suspense to stream header/hero first; move sidebar widgets to an island hydrated on idle; gate chart math behind useTransition.
- Re-measure. Report new LCP/INP/CLS and TBT.
- Add Lighthouse CI budgets (LCP < 2.5 s, CLS < 0.1, INP < 200 ms) and RUM collection for p95 monitoring.
Deliverable:
A before/after dossier with traces, diffs of request priorities and long tasks, and a CI budget config proving sustained Web Vitals improvements.

