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
Required
Marketing cookies
Personalization cookies
Analytics cookies
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.