How do you fix poor Web Vitals in React via profiling and CI?

Step-by-step profiling of network, main thread, and hydration—then precise React fixes.
Run a disciplined Core Web Vitals triage and apply code, image, and React concurrency fixes that measurably raise LCP, INP/FID, and CLS.

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

Area What I Measure Primary Fixes Expected Gain
Network LCP request order, render blockers Preload hero + fonts, defer third-party, code split entry Faster LCP
Main Thread Long tasks, parse/compile, layout Smaller ESM chunks, remove sync work, batch layout Lower TBT/INP
Hydration React mount/hydrate cost per route Streaming SSR, Suspense islands, server components Earlier interactivity
Images Bytes, decode time, CLS risk AVIF/WebP srcset, dimensions, LQIP, lazy load Stable CLS + faster paint
Fonts/CSS FOIT/FOUT, unused CSS font-display, size-adjust, critical CSS Stable text, lower CSS cost
CI/RUM p95 LCP/CLS/INP deltas Budgets + PR gates, Web Vitals in prod Regression control

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:

  1. 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.
  2. 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.
  3. Run React Profiler on the first route. Measure hydration time and name the top three expensive components.
  4. 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.
  5. Enable streaming SSR with Suspense to stream header/hero first; move sidebar widgets to an island hydrated on idle; gate chart math behind useTransition.
  6. Re-measure. Report new LCP/INP/CLS and TBT.
  7. 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.

Still got questions?

Privacy Preferences

Essential cookies — Google Analytics, Microsoft Clarity, Referly
Required
Marketing cookies
Personalization cookies
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.