The Web Performance Metrics That Actually Matter
Performance has a terminology problem.
Ask a frontend engineer about Core Web Vitals and you'll get a recitation of acronyms. LCP. INP. CLS. TTFB. FCP. Each one sounds important. But ask which ones actually affect your users — and which ones you should spend time improving — and the answer gets hazier.
This is a deep dive into the metrics that matter, what they're actually measuring, and the techniques that make a real difference. No fluff, no generic advice about "reducing bundle size."
The Three Metrics Google Cares About (And Why You Should Too)
Google's Core Web Vitals are a subset of performance metrics that directly influence search rankings. As of 2024, there are three:
- LCP (Largest Contentful Paint) — loading
- INP (Interaction to Next Paint) — interactivity
- CLS (Cumulative Layout Shift) — visual stability
INP replaced FID (First Input Delay) in March 2024. This is important: if your performance tooling or documentation still references FID, it's outdated.
Let's go through each one.
LCP: Largest Contentful Paint
What it measures
LCP marks the point when the largest visible content element — usually a hero image, a large heading, or a video thumbnail — finishes rendering. It's a proxy for "when does the page feel loaded?"
Good: under 2.5s | Needs improvement: 2.5–4s | Poor: over 4s
What causes poor LCP
The most common culprits, in order of frequency:
- Slow image loading. The largest element is usually an image, and if it's unoptimized, large in file size, or discovered late (loaded via CSS background or JavaScript), LCP suffers.
- Render-blocking resources. Stylesheets and scripts in
<head>that block the browser from painting. - Slow server response. If the HTML itself takes 2 seconds to arrive, everything else starts late.
- No preloading. The browser discovers the LCP image late because it's buried in CSS or dynamically injected.
How to fix it
Use priority on your LCP image. In Next.js, the <Image> component with priority preloads the image and prevents lazy loading:
<Image src={heroImage} alt="Hero" priority />
This alone is the highest-impact LCP fix for most Next.js sites.
Preload critical images explicitly. If you can't use the Image component, add a preload hint in <head>:
<link rel="preload" as="image" href="/hero.webp" />
Use modern image formats. WebP and AVIF are significantly smaller than JPEG at the same quality. Next.js handles format conversion automatically with <Image> — use it.
Eliminate render-blocking CSS. Inline critical CSS and defer the rest. This is more relevant for custom setups; Next.js handles most of this automatically via its CSS extraction.
INP: Interaction to Next Paint
What it measures
INP measures the latency between a user interaction (click, keypress, tap) and the next visual update. It replaced FID because FID only measured the delay before an event handler ran — INP measures the full round trip, including the time to actually paint the response.
Good: under 200ms | Needs improvement: 200–500ms | Poor: over 500ms
This is the metric most frontend engineers underestimate. A component that re-renders a large list every time a user types, a button that triggers a heavy computation on click, a modal that causes layout recalculation — all of these hit INP.
What causes poor INP
Long tasks on the main thread. The browser can only do one thing at a time. If your JavaScript is running a 300ms synchronous task, every interaction during that window will feel unresponsive.
Excessive re-renders. React components that re-render unnecessarily on every state change add up, especially when the component tree is large.
Third-party scripts. Analytics, chat widgets, and ad scripts often run on the main thread and compete with your code for time.
How to fix it
Profile first. Open Chrome DevTools, go to the Performance panel, record an interaction, and look for long tasks (the red triangles). Don't optimize blind.
Debounce expensive operations. If a user input triggers a filter, a search, or any heavy computation, debounce it:
const debouncedSearch = useMemo(
() => debounce((query: string) => performSearch(query), 200),
[],
)
Break up long tasks with scheduler.yield() (or setTimeout(fn, 0) as a fallback). This lets the browser handle pending interactions between chunks of work:
async function processLargeList(items: Item[]) {
for (const item of items) {
processItem(item)
if (shouldYield()) await scheduler.yield()
}
}
Memoize aggressively where it matters. React.memo, useMemo, and useCallback aren't premature optimization when applied to components that render frequently with stable props.
Audit third-party scripts. Use the Coverage panel in DevTools to see how much of each script actually executes. Lazy-load non-critical third parties:
// Load chat widget only after user interaction
useEffect(() => {
const handleFirstInteraction = () => {
loadChatWidget()
window.removeEventListener('click', handleFirstInteraction)
}
window.addEventListener('click', handleFirstInteraction, { passive: true })
}, [])
CLS: Cumulative Layout Shift
What it measures
CLS measures how much the layout shifts unexpectedly during the page lifetime. A score of 0 is perfect (nothing shifted). Anything above 0.1 is noticeable, above 0.25 is jarring.
Good: under 0.1 | Needs improvement: 0.1–0.25 | Poor: over 0.25
You know CLS when you feel it: you're about to click a button and an image above it loads, pushing the button down, and you accidentally click something else.
What causes poor CLS
- Images without explicit dimensions
- Ads, embeds, or iframes that load with unknown dimensions
- Fonts causing text to reflow (FOUT — Flash of Unstyled Text)
- Content injected above existing content (banners, cookie notices)
- Animations that move elements in a way that affects layout
How to fix it
Always specify width and height on images. The browser uses these to reserve space before the image loads. Next.js's <Image> component enforces this, which is one of the reasons it exists:
<Image src={photo} alt="Profile" width={400} height={400} />
Use aspect-ratio CSS for unknown-dimension content. For embeds and iframes where you know the ratio but not exact dimensions:
.video-wrapper {
aspect-ratio: 16 / 9;
width: 100%;
}
Use font-display: optional or swap for custom fonts. optional prevents any reflow by only using the custom font if it loads fast enough. swap shows fallback text immediately and swaps when the font loads.
Avoid inserting content above the fold. Cookie banners, notification bars, and promotions injected above existing content are CLS killers. Reserve space for them, or anchor them to the bottom of the screen instead.
Tooling: How to Measure
PageSpeed Insights
The simplest starting point. Paste a URL, get lab data (Lighthouse) and field data (real user data from CrUX). Field data is more representative; lab data is more reproducible for testing fixes.
Chrome DevTools Performance Panel
For diagnosing specific issues. Record a session, look for long tasks, layout shifts (purple bars), and paint events. This is where you'll find the root cause of INP and CLS issues.
web-vitals JavaScript library
For measuring in production with real users:
import { onLCP, onINP, onCLS } from 'web-vitals'
onLCP(console.log)
onINP(console.log)
onCLS(console.log)
Send these to your analytics or observability platform. Lab scores don't always reflect what real users experience — field data closes that gap.
Vercel Analytics
If you're on Vercel, Web Analytics is built in and surfaces Core Web Vitals from real traffic with no extra setup. It's the lowest-effort option for Next.js projects.
The 80/20 of Web Performance
If you want to improve your Core Web Vitals without a week-long audit, start here:
- Add
priorityto your LCP image - Specify dimensions on all images
- Debounce event handlers that trigger expensive work
- Defer third-party scripts
- Check your font loading strategy
Most sites see significant improvement from just the first two. The rest is refinement.
Performance isn't about perfect scores. It's about making sure users don't notice the infrastructure. When the metrics are green, you've done your job.