React Performance and Core Web Vitals: A Practical Guide
Diagnose and improve LCP, INP, and CLS in React with field data, browser profiling, rendering fixes, image strategy, and measured memoization.

React performance work goes wrong when a team starts with memo, useMemo, or a bundle analyzer before identifying what users actually experience. Core Web Vitals give us three user-centered outcomes—loading, responsiveness, and visual stability—but the metric is only the starting point. The useful question is: which part of the page's work caused this result?
This guide gives you a repeatable diagnostic order for React and Next.js applications. It begins with field data, moves through a reproducible browser trace, and applies the smallest fix that addresses the measured bottleneck.
Know the current Core Web Vitals thresholds
Google's current Web Vitals guidance defines these “good” thresholds at the 75th percentile of page loads, evaluated separately for mobile and desktop:
| Metric | What it represents | Good threshold |
|---|---|---|
| LCP | Loading of the largest visible content element | 2.5 seconds or less |
| INP | Responsiveness across user interactions | 200 milliseconds or less |
| CLS | Unexpected visual movement | 0.1 or less |
A single fast laptop run does not prove that a site passes. Field data includes real devices, networks, caches, extensions, and interaction patterns. Lab tools are still valuable because they are reproducible and explain causes, but they answer a different question.
Use a field-first diagnostic workflow
Follow this order:
- 1.Confirm whether the problem exists in real-user data such as CrUX, PageSpeed Insights field data, Search Console, or your own RUM.
- 2.Segment by page type, device, navigation type, geography, and application version where possible.
- 3.Reproduce the slow path in a lab with realistic CPU and network conditions.
- 4.Record a performance trace and identify the responsible resource, task, component, or layout shift.
- 5.Make one meaningful change.
- 6.Compare lab results, deploy, then wait for enough field observations to judge the real effect.
Do not mix page types too early. A marketing page with a large hero image and an authenticated data grid can fail the same metric for completely different reasons.
Diagnose LCP as four separate intervals
LCP measures when the largest image, text block, or other eligible content element in the viewport finishes rendering. The web.dev LCP guide breaks the total into:
- 1.Time to First Byte
- 2.Resource load delay
- 3.Resource load duration
- 4.Element render delay
That breakdown prevents random optimization.
If TTFB is slow
Investigate server work, database queries, redirects, cache misses, and distance from the serving region. For static content, prerendering and CDN caching may remove runtime rendering from the critical path. For dynamic content, cache safe results and parallelize independent requests.
export default async function ProductPage({ params }) {
const { id } = await params;
const [product, recommendations] = await Promise.all([
getProduct(id),
getRecommendations(id),
]);
return <ProductView product={product} recommendations={recommendations} />;
}The parallel form avoids a request waterfall when the operations do not depend on each other.
If resource load delay is slow
The browser discovered the LCP resource too late. Common causes include a hero image inserted by client JavaScript, a CSS background image hidden in a late stylesheet, or a preload scanner that cannot see the final markup.
Render the image in the initial HTML. If it is definitely the LCP candidate, give it appropriate priority and do not lazy-load it.
import Image from "next/image";
<Image
src="/hero.webp"
alt="Dashboard showing the completed workflow"
width={1600}
height={1000}
sizes="(max-width: 768px) 100vw, 72vw"
priority
/>Do not set priority on every image. Competing high-priority requests can make the critical one slower.
If resource load duration is slow
Serve an appropriately sized image, compress it, use a modern format when supported, and check CDN/cache behavior. The Next.js Image component can provide responsive sizing, modern formats, lazy loading for noncritical images, and reserved dimensions.
If element render delay is slow
The resource may already be downloaded while the browser waits for JavaScript, CSS, fonts, or an entrance animation. A common design mistake is hiding the hero with opacity: 0 until a large animation bundle initializes. The page may feel “cinematic” on a fast machine and blank on a slower one.
Keep the meaningful hero state visible without JavaScript. Enhance it after paint. If motion is essential, animate a mask, line, or secondary layer without delaying the primary heading or image.
Improve INP by shortening the interaction's critical path
INP covers the latency of interactions across a visit. An interaction contains input delay, event-handler processing, and presentation delay. The web.dev INP guide recommends identifying which part dominates before changing code.
Reduce input delay
Input delay usually means the main thread was busy before the handler could run. Look for long startup tasks, third-party scripts, large hydration work, and expensive synchronous parsing.
- ●Send less client JavaScript.
- ●Split code by route or interaction.
- ●Delay nonessential third-party scripts.
- ●Move static UI back to Server Components.
- ●Break long work into smaller tasks so the browser can respond.
This is where good Server/Client boundaries matter. Read the React Server Components guide if a page currently marks an entire route as a Client Component.
Reduce processing duration
An event handler should update what the user needs immediately and defer work that can wait.
"use client";
import { useState, useTransition } from "react";
export function Search({ items }) {
const [query, setQuery] = useState("");
const [results, setResults] = useState(items);
const [isPending, startTransition] = useTransition();
function handleChange(event) {
const value = event.target.value;
setQuery(value);
startTransition(() => {
setResults(filterItems(items, value));
});
}
return <SearchView query={query} results={results} pending={isPending} onChange={handleChange} />;
}A transition does not make expensive work free. It allows urgent updates to stay responsive while nonurgent rendering proceeds. If filterItems itself blocks for a long time, move the calculation to a worker, server, index, or better data structure.
Reduce presentation delay
After handlers finish, the browser still needs to render. Large DOM trees, expensive style recalculation, synchronous layout reads, and complex paint effects can delay the next frame.
Avoid alternating DOM reads and writes in a loop. Virtualize genuinely large lists. Consider content-visibility: auto for substantial off-screen sections, but test focus navigation and layout behavior before shipping it.
Fix CLS by reserving the final geometry
CLS is often less about speed and more about uncertainty. The browser lays out a page, then content arrives and changes the geometry.
Frequent causes are:
- ●Images or video without dimensions
- ●Web fonts with very different fallback metrics
- ●Banners inserted above existing content
- ●Skeletons that do not match loaded content
- ●Animations that change layout properties
- ●Responsive components whose server and client states disagree
Reserve dimensions with width and height, an aspect ratio, or a stable container. Next.js Image uses intrinsic dimensions to preserve aspect ratio. For fonts, next/font self-hosts files and provides options designed to reduce layout shift; see the official Font module.
Animate transforms and opacity for secondary motion instead of top, left, width, or height when the visual result allows it. More importantly, do not insert a late-loading consent bar, promotion, or alert above content without reserving its space.
Profile React rendering after browser-level diagnosis
Core Web Vitals tell you about the page; React Profiler tells you about React commits. Use both.
The interactive Profiler in React Developer Tools shows which components rendered and how long a commit took. The [<Profiler> API](https://react.dev/reference/react/Profiler) can collect measurements programmatically in a profiling build.
import { Profiler } from "react";
function onRender(id, phase, actualDuration, baseDuration) {
console.table({ id, phase, actualDuration, baseDuration });
}
<Profiler id="Results" onRender={onRender}>
<Results items={items} />
</Profiler>Profile a production build or an appropriate profiling build. Development mode adds checks and overhead that can distort conclusions.
Memoize only a measured bottleneck
memo skips a component render when its props are unchanged by React's comparison. useMemo caches a calculation result between renders. Both have costs: dependency tracking, comparison, memory, and cognitive complexity.
Use them when all of these are true:
- 1.Profiling shows repeated work is meaningfully expensive.
- 2.Inputs are stable often enough for caching to hit.
- 3.The comparison or cache costs less than the work it avoids.
- 4.The component remains correct without memoization.
React's useMemo documentation explicitly describes it as a performance optimization, not a semantic guarantee. The current memo documentation also notes that a freshly created object or function prop can defeat memoization.
const visibleRows = useMemo(
() => expensiveSortAndFilter(rows, filters),
[rows, filters]
);
const Results = memo(function Results({ rows }) {
return rows.map((row) => <ResultRow key={row.id} row={row} />);
});Do not wrap every component automatically. Often the better fix is local state, smaller components, fewer effects, stable data flow, or removing an unnecessary client boundary.
Audit third-party scripts and animation
Analytics, chat, A/B testing, maps, video embeds, and tag managers can dominate main-thread time. For each third party, document:
- ●Who owns it
- ●Which pages need it
- ●When it loads
- ●What business decision depends on it
- ●How it affects bytes, long tasks, and network priority
Load it only where needed and choose a strategy appropriate to its dependency on the initial page. Consent requirements may also control when it can execute.
Animation should communicate state or hierarchy. Avoid perpetual work and large scroll handlers that read layout on every frame. For scroll-linked effects, batch DOM reads, update through requestAnimationFrame, and animate compositor-friendly properties where possible. Test on a modest mobile device, not only a development workstation.
Collect useful field measurements
The web-vitals package can report the current metrics to your analytics endpoint:
import { onCLS, onINP, onLCP } from "web-vitals";
function send(metric) {
navigator.sendBeacon(
"/analytics/vitals",
JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id,
path: location.pathname,
})
);
}
onCLS(send);
onINP(send);
onLCP(send);Respect privacy, sampling, consent, and data-retention rules. Store enough context to diagnose a template or release, but do not collect sensitive data you do not need.
A practical performance checklist
Loading and LCP
- ●Find the actual LCP element in field or lab data.
- ●Break LCP into TTFB, resource delay, resource duration, and render delay.
- ●Render the critical content in initial HTML.
- ●Do not lazy-load the LCP image.
- ●Avoid hiding the primary content behind JavaScript animation.
Responsiveness and INP
- ●Record the slow interaction, not only initial page load.
- ●Find long tasks occupying the main thread before input.
- ●Keep handlers small and defer nonurgent rendering.
- ●Reduce unnecessary hydration and third-party JavaScript.
- ●Inspect DOM size, layout, and paint after the handler completes.
Stability and CLS
- ●Reserve media and embed dimensions.
- ●Match skeleton and loaded-content geometry.
- ●Control font metrics and loading.
- ●Avoid inserting content above what the user is reading.
- ●Test responsive transitions and restored navigation states.
React-specific checks
- ●Profile the relevant interaction in a production-like build.
- ●Locate the expensive commit and component.
- ●Reduce state scope and effect chains before caching everything.
- ●Add
memooruseMemoonly when measurements justify it. - ●Re-measure after each meaningful change.
The principle that keeps performance work honest
Optimize the user's waiting, not the framework's counters. A smaller bundle can still produce a slow interaction. Fewer React renders can still leave a late LCP image. A perfect lab score can hide poor field performance on one device group.
Start with the real symptom, trace it to a specific delay, and make the smallest change that removes that delay. That process is slower than collecting generic tips—but it produces improvements that survive production.