React Website Performance: Fix LCP, INP, CLS, and TTFB

Sandor Farkas
Founder & Lead Developer
Expert in software development and legacy code optimization
LinkedIn
Short answer: Measure real-user data first, then fix one metric at a time. For LCP, get the hero image or headline into the initial HTML and load it with high priority. For INP, keep input handlers cheap and move expensive re-renders into transitions. For CLS, reserve space for everything that loads late. For TTFB, cache HTML at the edge and remove server-side waterfalls. Google's "good" thresholds are unchanged in 2026: LCP at or below 2.5 seconds, INP at or below 200 milliseconds, and CLS at or below 0.1, measured at the 75th percentile of real visits.
If your React website feels “fine on my machine” but users see jank, slow hero rendering, or flaky Lighthouse scores, you are usually fighting several different problems with one vague label: “performance.” The fastest way to make progress is to separate them:
- LCP (Largest Contentful Paint): how quickly the main content becomes visible.
- INP (Interaction to Next Paint): how quickly the page responds when users click, tap or type.
- CLS (Cumulative Layout Shift): how stable the page is while it loads.
- TTFB (Time to First Byte): how quickly the server (and network) start responding.
LCP, INP and CLS are the three Core Web Vitals. INP replaced First Input Delay (FID) in March 2024, and for React apps it is often the hardest of the three to pass, because it measures exactly the main-thread work that heavy client-side rendering creates. TTFB is not a Core Web Vital, but it sits underneath LCP: every millisecond the server spends before the first byte is a millisecond the hero cannot paint.
Google’s guidance on Core Web Vitals is explicit about thresholds and measurement: prioritize field data, then use lab tools to reproduce and debug issues (web.dev on Core Web Vitals). This article gives a practical, React-specific playbook to diagnose and fix each metric without cargo-cult tuning.
What LCP, INP, CLS, and TTFB actually measure (and why React teams confuse them)
React apps often blur boundaries between “server slow,” “frontend slow,” and “data slow” because rendering frequently depends on JavaScript and API calls. Start by aligning on the definitions.
| Metric | What it measures | “Good” target (Google) | Common React website causes |
|---|---|---|---|
| LCP | When the largest element in the viewport becomes visible | ≤ 2.5s | Heavy hero images, render blocking CSS/JS, hydration cost, data fetching waterfalls |
| INP | Delay from a user interaction to the next frame painted | ≤ 200ms | Expensive re-renders on input, long hydration tasks, heavy third-party scripts |
| CLS | Visual stability, unexpected layout movement | ≤ 0.1 | Images without dimensions, late-loading banners, font swaps, skeletons that change height |
| TTFB | Time until first byte of HTML is received | ≤ 0.8s (good), slower needs attention | No CDN caching, slow SSR/data queries, cold starts, too many redirects |
Two important clarifications:
- TTFB is not “React is slow.” If your HTML arrives quickly but the page is blank until JS loads and data returns, that is typically LCP and main-thread work, not TTFB.
- CLS is rarely a single bug. It is usually many small “unreserved space” issues across images, components, and late UI.
Step 1: Measure the right way (field first, lab second)
Performance work becomes political when you cannot agree on the baseline. Use a simple measurement stack that produces answers like “p75 LCP on product detail pages in the US on mobile.”
Field data (what users experience)
Use at least one of these:
- Chrome UX Report (CrUX) via PageSpeed Insights or BigQuery for real-user Core Web Vitals where available (CrUX docs).
- RUM (Real User Monitoring), either from your observability vendor or via your own beaconing.
- The lightweight
web-vitalslibrary to capture LCP, INP, CLS and TTFB in production (GitHub: web-vitals).
A minimal “don’t overthink it” instrumentation example:
import { onCLS, onINP, onLCP, onTTFB } from 'web-vitals'
function sendToAnalytics(metric: { name: string; value: number; id: string }) {
navigator.sendBeacon(
'/rum',
JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
path: location.pathname,
})
)
}
onCLS(sendToAnalytics)
onINP(sendToAnalytics)
onLCP(sendToAnalytics)
onTTFB(sendToAnalytics)
If you do one thing, do this: tag metrics by route/template (home, category, detail, checkout, marketing landing). Averages hide the pages that matter.
Lab data (to reproduce and debug)
- Lighthouse and PageSpeed Insights for repeatable audits.
- Chrome DevTools Performance panel to see main-thread work, layout shifts, long tasks, and the LCP element.
- React Profiler for component-level render cost.
Lab data is not truth, but it is the fastest way to locate the actual cause once field data tells you which page is failing.

Step 2: Fix LCP in a React website (treat the hero path as a product feature)
LCP is usually “the biggest thing users care about” on that page: hero image, product photo, headline block, or a large card.
Find your LCP element and why it is late
In Chrome DevTools (Performance or Lighthouse details) you can see the LCP element and its timing breakdown. Google splits LCP into four subparts, and knowing which one dominates tells you where to look:
- TTFB: the server is slow to start responding (see Step 5).
- Resource load delay: the browser discovers the hero image late, for example because only JavaScript knows its URL.
- Resource load duration: the file itself is too large or served from too far away.
- Element render delay: the resource has arrived, but rendering waits on CSS, fonts, JavaScript or hydration.
Common patterns:
- The LCP element is an image that loads too slowly.
- The LCP element is text delayed by fonts, CSS, or JS.
- The LCP element depends on data that arrives after one or more client-side waterfalls.
High-impact LCP fixes (React-specific)
1) Make the LCP resource load earlier (usually images)
If the LCP is an image, your first goal is to ensure the browser discovers it immediately and downloads an appropriately sized file.
- Use responsive images (
srcset,sizes) so mobile does not download desktop assets. - Prefer modern formats (AVIF/WebP) where your pipeline supports them.
- Avoid shipping the hero image only after client-side routing state resolves.
- Preload the hero image when it is known at build time.
- Mark the LCP image with
fetchPriority="high"so the browser does not queue it behind less important images, and never lazy-load it.
If your stack supports SSR/SSG, ensure the hero image URL is present in the initial HTML so discovery does not depend on hydration.
React 19 made the preload part easier. react-dom exports resource hint functions (preload, preinit, preconnect, prefetchDNS) that you can call from any component, and React places the resulting tags in the document head, including during server rendering:
import { preload } from 'react-dom'
export function ProductHero({ product }: { product: Product }) {
preload(product.heroImageUrl, { as: 'image', fetchPriority: 'high' })
return (
<img
src={product.heroImageUrl}
alt={product.name}
width={1200}
height={800}
fetchPriority="high"
/>
)
}
The component that knows which image is the hero is the one that declares it, and the hint still ends up in the head where the browser sees it early. In Next.js, next/image handles the same concern through its own props.
2) Reduce render-blocking work before the hero can paint
React websites often delay LCP because too much JavaScript must execute before meaningful content appears.
Practical tactics that work across SPA and SSR setups:
- Route-level code splitting using
React.lazy()and dynamic imports, so your marketing route does not download your entire app. - Remove or defer non-critical scripts (analytics, chat widgets) until after the first paint.
- Audit dependencies that inflate bundles (date libraries, editor toolkits, charting libs) and isolate them to the routes that need them.
Bundle analysis is frequently the fastest win. Most build tools can visualize chunk composition.
3) Prevent data-fetching waterfalls on the critical path
A common LCP killer in React is “render, then fetch, then render again, then fetch more.” This tends to happen when:
- Components fetch their own data independently.
- Auth/session checks block page data.
- The API shape forces multiple sequential calls.
Fixes:
- Fetch critical data at the route boundary, not deep in the tree.
- Parallelize independent calls.
- Add a BFF (Backend for Frontend) or aggregation endpoint when the UI needs a single view model.
- Cache server responses aggressively for public content.
If you want a broader architecture framing for React teams, Wolf-Tech’s guide on seams, state types, and boundaries helps prevent these performance traps: React Front End Architecture for Product Teams.
4) Fonts: make text paint fast and predictably
When LCP is a large heading, font loading can delay it.
- Preconnect to font origins if using a font CDN.
- Use
font-display: swapso text renders promptly. - If you see layout shifts due to font swapping, address that under CLS (below) with font metric overrides.
Google’s general guidance on optimizing LCP emphasizes prioritizing critical resources and reducing render-blocking work (Optimize LCP).
Step 3: Fix INP (keep the main thread free when users interact)
INP measures the time from a click, tap or key press to the next frame the browser paints, and reports roughly the worst interaction of a visit. A React page can load fast and still fail INP, because the problem shows up later: a filter that re-renders a 500-row table on every keystroke, a menu button that waits behind a hydration task, a chat widget that runs a long task every few seconds.
Find the slow interactions
Field data tells you which page fails. To find which interaction, the web-vitals attribution build (web-vitals/attribution) reports the target element and splits each INP value into input delay, processing time and presentation delay. In the lab, record the interaction in the DevTools Performance panel and look for long tasks (over 50 milliseconds) between the input event and the next paint. The React Profiler shows which components re-rendered and how long they took.
High-impact INP fixes (React-specific)
1) Split urgent and non-urgent updates
The input itself must update immediately. The expensive result (a filtered list, a chart, a search result) can wait a frame. React's transitions exist for exactly this:
import { useState, useTransition } from 'react'
function ProductFilter({ products }: { products: Product[] }) {
const [query, setQuery] = useState('')
const [filter, setFilter] = useState('')
const [isPending, startTransition] = useTransition()
return (
<>
<input
value={query}
onChange={(e) => {
setQuery(e.target.value) // urgent: keep typing smooth
startTransition(() => setFilter(e.target.value)) // can be interrupted
}}
/>
<ProductTable products={products} filter={filter} dimmed={isPending} />
</>
)
}
useDeferredValue gives the same effect when you do not control the state update, for example when the value comes from a parent. Transition renders are interruptible, so a new keystroke abandons the stale render instead of queuing behind it.
2) Render less on each interaction
- Keep state close to where it is used, so a change in one form field does not re-render the whole page.
- Virtualize long lists and tables instead of rendering every row.
- Let the React Compiler handle memoization where you can. It became stable with React Compiler 1.0 in October 2025, and Next.js 16 supports it through the
reactCompileroption. It removes most of the unnecessary re-renders that manualuseMemoandmemocalls were meant to prevent.
3) Ship and hydrate less JavaScript
Hydration runs as long tasks, and an interaction that lands during hydration waits for it. Server Components (in Next.js or another RSC framework) keep non-interactive parts of the page out of the client bundle entirely. In a client-rendered app, route-level code splitting and deferring below-the-fold widgets have the same effect on a smaller scale.
4) Break up long tasks you cannot avoid
If an event handler has to do real work (parsing a file, computing a large diff), give the browser a chance to paint first. Update the UI to show progress, then continue the work after yielding, with scheduler.yield() where the browser supports it and a setTimeout fallback elsewhere. Third-party scripts are the other common source of long tasks: load them after interaction or in a web worker where the vendor supports it.
Reference: web.dev on optimizing INP.
Step 4: Fix CLS (stability is a design and engineering contract)
CLS is frequently underestimated because the page “looks fine” to developers on fast connections. In the field, users see late banners, resizing images, and UI that jumps when data arrives.
The CLS root causes you should check first
1) Images and media without reserved space
If any content loads later into a box that had no explicit size, the browser must reflow the page.
- Always provide
widthandheighton images, or use CSSaspect-ratio. - For responsive layouts, reserve space using a container with a fixed aspect ratio.
- Treat embeds (iframes, videos) the same way.
2) Skeletons and placeholders that do not match final layout
Skeleton UIs are great, until the skeleton height differs from the final content.
- Use stable placeholders that match the final component dimensions.
- Avoid “loading spinners” that collapse a section and then expand it.
3) Late UI inserted above existing content
Common offenders:
- Cookie consent bars that push content down.
- Promo banners injected after hydration.
- Form validation summaries that appear at the top after submission.
Prefer overlays for late UI, or reserve space for banners from the start.
4) Font swaps causing layout shift
Even with font-display: swap, swapping to a web font can change text width and line breaks.
Mitigations:
- Use fallback fonts with similar metrics.
- Consider
size-adjust,ascent-override,descent-override, andline-gap-overridein@font-faceto align metrics and reduce shift.
Reference: web.dev on preventing layout shifts.
A practical CLS checklist you can hand to a team
- Ensure all images, iframes, and video containers reserve space.
- Reserve space (or use overlays) for banners and consent UI.
- Make skeletons dimensionally consistent with final components.
- Avoid inserting content above the fold after initial render.
- Use transform-based animations (
transform,opacity), not layout-changing animations (top,height).

Step 5: Fix TTFB (separate caching problems from backend latency)
TTFB is a server and delivery metric. For a React website, it depends on whether you serve static HTML, SSR, or an app shell.
Diagnose TTFB with a simple breakdown
When TTFB is high, it is usually one of these:
- Too far from the user (no CDN, wrong region, no edge caching).
- Too much server work per request (SSR doing heavy work, slow APIs, slow database queries).
- Cold starts (serverless functions waking up, new containers).
- Redirect chains (http to https, www to apex, locale redirects).
Before changing code, confirm the bottleneck:
- Use server timing headers (
Server-Timing) to surface SSR time, DB time, cache hit or miss. - Use tracing (OpenTelemetry, APM vendor) to pinpoint where the request spends time.
High-leverage TTFB fixes
1) Cache the HTML (or avoid generating it per request)
If your “website” pages are mostly public and content-driven, pushing them toward static generation or cached SSR usually yields the biggest TTFB improvements.
- Put a CDN in front of the origin.
- Use cache headers that enable edge caching.
- Ensure personalized content does not accidentally disable caching for everyone.
2) Remove server-side waterfalls
If SSR waits on multiple backend calls, TTFB grows with each dependency.
- Parallelize server-side fetches.
- Add aggregation endpoints for page-level view models.
- Cache expensive computations.
3) Fix backend hot paths (database and APIs)
TTFB is often a symptom of slow data access.
- Identify slow queries, add the right indexes, and eliminate N+1 patterns.
- Use connection pooling appropriately.
- Move non-critical work off the request path.
If you want a broader “system-level” optimization mindset (beyond micro-tuning), this Wolf-Tech article is a good companion: Optimize the Code: High-Impact Fixes Beyond Micro-Optimizing.
4) Eliminate redirect chains
Redirects are easy to miss and expensive on mobile.
- Collapse multiple redirects into one.
- Fix canonical host and protocol at the edge.
Step 6: Prevent regressions with budgets and guardrails
A one-time performance sprint helps, but performance decays unless you make it part of delivery.
Add performance budgets that match your business
You do not need an elaborate system. Start with budgets tied to what you already measure:
| Guardrail | What it prevents | How to enforce |
|---|---|---|
| LCP, INP and CLS thresholds per key route | Slow hero path, sluggish input, layout instability | RUM alerts, weekly p75 review |
| Bundle size checks | “Just one more dependency” growth | CI step, bundle analyzer in PR |
| Third-party script policy | Tag sprawl | Require review, load after interaction |
| Synthetic checks for top pages | Silent breakages | Scheduled Lighthouse runs |
Make performance ownership explicit
The most reliable pattern is to treat performance as a cross-functional contract:
- Product defines which routes are revenue-critical.
- Design commits to stable layout patterns.
- Engineering commits to budgets and enforcement.
- Ops commits to caching and latency SLOs.
Wolf-Tech’s approach to “testable proofs” in front-end delivery aligns well with performance work, because it makes budgets and evidence part of the definition of done: Front End Development Services: Deliverables That Matter.
A pragmatic 10-day plan to improve a React website
If you want momentum without turning this into a quarter-long initiative:
Days 1 to 2: Baseline and pick target routes
Focus on 2 to 4 templates that drive conversions or acquisition.
Days 3 to 6: Fix the biggest LCP, INP and CLS issues per route
LCP and CLS improvements usually ship in small, reviewable changes: image sizing, preloads, removing a blocking script, reserving layout space. For INP, start with the two or three slowest interactions from your attribution data and apply transitions or reduce the re-render scope.
Days 7 to 8: TTFB and caching pass
Add server timing, fix redirects, validate CDN behavior, and tackle the slowest SSR or API path.
Days 9 to 10: Add guardrails
Introduce budgets, CI checks, and basic RUM alerting so you do not lose the gains.
When you should bring in an outside performance review
Consider an external audit when:
- Field data shows persistent p75 failures and you cannot reproduce reliably.
- You have a mixed rendering setup (SPA plus SSR) and ownership is unclear.
- Third-party scripts are politically hard to control.
- Backend latency and frontend rendering are both suspects.
Wolf-Tech helps teams build, optimize, and scale production React and full-stack systems, including performance-focused reviews and modernization work. If you want a measurement-first plan that targets LCP, INP, CLS, and TTFB with clear proof and regression guardrails, start at Wolf-Tech and we can scope a short, high-signal engagement.
