I like performance work because it rewards honesty. You cannot talk a slow page into feeling fast. You have to understand what the user sees, what the browser is doing, and where the friction comes from. Core Web Vitals help because they turn vague complaints like "this feels heavy" into measurable signals. But the numbers only become useful when you interpret them through the experience of an actual visitor.
What Core Web Vitals are really measuring
At a high level, the vitals tell you whether a page loads a meaningful visual quickly, remains visually stable, and responds well when the user tries to interact. That matters because users do not experience performance as one number. They experience it as a sequence. Did something useful appear? Did the layout jump? Did the page respond when I tapped or clicked?
That is why I like these metrics. They map more closely to perception than raw page weight alone. A page can be technically loaded and still feel bad. The vitals help expose that gap.
How I think about the big three
Largest Contentful Paint
Largest Contentful Paint is about when the main visible content becomes available. If the hero image is oversized, the server is slow, fonts block rendering, or the route fetches too much before showing anything useful, LCP suffers. I usually look there first on content heavy or marketing pages because it shapes the initial impression immediately.
A strong LCP often comes from simpler decisions: better image handling, and less unnecessary work in the critical path.
Cumulative Layout Shift and interaction responsiveness
Layout shift is the page telling on sloppy reservation of space. Images without dimensions, injected banners, unstable ads, and late loading UI are common causes. I hate layout shift because users feel it as broken trust. They go to tap one thing and the page moves underneath them.
Interaction responsiveness exposes a different kind of friction. When the page looks ready but JavaScript blocks input or a heavy component ties up the main thread, people feel that lag immediately.
LCP optimization with next/image
In Next.js projects, the next/image component handles most LCP optimization automatically. It generates responsive srcsets, serves modern formats like WebP and AVIF, and lazy loads images below the fold. For the hero image (the LCP element), I set priority to disable lazy loading and preload it:
import Image from 'next/image';
export function HeroSection() {
return (
<section className="hero">
<Image
src="/images/hero-project.png"
alt="Featured project showcase"
width={1200}
height={675}
priority
sizes="100vw"
quality={85}
/>
<h1>Building products that perform</h1>
</section>
);
}The priority prop tells Next.js to preload this image in the HTML head, which means the browser starts downloading it before it even parses the component tree. The sizes prop helps the browser pick the right srcset image for the viewport width. And quality={85} keeps file sizes reasonable without visible degradation.
For pages that do not use Next.js, the equivalent is adding explicit width and height attributes, using loading="eager" for the hero image, and adding a <link rel="preload"> tag in the head.
Font display swap and CLS
Custom fonts are a common source of both LCP delays and CLS. If the browser waits for a font file before rendering any text, the user sees a blank page longer than necessary. I always use font-display: swap so the browser shows the fallback font immediately and swaps in the custom font when it arrives:
@font-face {
font-family: 'Kanit';
src: url('/fonts/kanit-v15-latin-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}To minimize the layout shift when the swap happens (because the custom font may have different metrics than the fallback), I use the size-adjust property on the fallback or match fallback fonts that are metrically similar. In Next.js, the next/font module handles this automatically by generating adjusted fallback fonts.
Explicit dimensions for CLS
The single biggest CLS fix is giving images and media explicit width and height attributes so the browser can reserve space before the asset loads:
<!-- Bad: no dimensions, causes layout shift -->
<img src="/cover.png" alt="Project cover">
<!-- Good: browser reserves a 16:9 box immediately -->
<img src="/cover.png" alt="Project cover" width="1200" height="675">For responsive images where I want CSS to control the rendered size, I combine explicit attributes with CSS:
.responsive-img {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
object-fit: cover;
}The aspect-ratio property is now well supported and eliminates the old padding-bottom hack for responsive aspect ratios. Combined with explicit width/height attributes, the browser can calculate the reserved space instantly.
Other common CLS sources I watch for: dynamically injected banners or cookie notices (I reserve space for them in the layout), web fonts loading with different metrics than the fallback (addressed with font-display: swap and metric adjustments), and third party embeds like maps or videos (I wrap them in containers with explicit aspect ratios).
Defer and async script patterns
Scripts that block rendering are one of the easiest performance problems to fix. The general rule: use defer for scripts that need the DOM, use async for scripts that are independent and order does not matter, and avoid inline scripts in the head that do heavy work:
<!-- Render blocking: avoid this -->
<script src="/analytics.js"></script>
<!-- Deferred: executes after HTML parsing, maintains order -->
<script src="/app.js" defer></script>
<!-- Async: executes as soon as downloaded, no order guarantee -->
<script src="/analytics.js" async></script>
<!-- Module scripts are deferred by default -->
<script type="module" src="/app.mjs"></script>For third party scripts like analytics, chat widgets, and tracking pixels, I load them with async and often delay their initialization until after the page is interactive. A chat widget that loads at the same time as the hero image is competing for bandwidth and main thread time.
INP: Interaction to Next Paint replacing FID
In March 2024, Google replaced First Input Delay (FID) with Interaction to Next Paint (INP) as a Core Web Vital. FID only measured the delay before the browser started processing the first interaction. INP measures the full latency of all interactions throughout the page lifecycle, from the user's input to the next paint that reflects the result.
This is a stricter metric and it catches problems that FID missed. A page could pass FID easily because the first click happened during a quiet period, but fail INP because later interactions (like opening a dropdown, filtering a list, or submitting a form) triggered heavy JavaScript that blocked the main thread.
To optimize INP, I focus on keeping event handlers fast, breaking up long tasks with requestAnimationFrame or scheduler.yield(), and avoiding synchronous work in response to user input. In React, startTransition helps by marking state updates as non-urgent so they do not block the next paint:
import { startTransition, useState } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
function handleInput(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value);
startTransition(() => {
setResults(filterProjects(e.target.value));
});
}
return (
<div>
<input value={query} onChange={handleInput} />
<ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>
</div>
);
}Measuring with the web-vitals library
Google's web-vitals library gives me real user metrics in production. I set it up to report to my analytics endpoint so I can see actual field data, not just lab scores:
import { onCLS, onINP, onLCP } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
delta: metric.delta,
id: metric.id,
page: window.location.pathname,
});
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/vitals', body);
} else {
fetch('/api/vitals', { body, method: 'POST', keepalive: true });
}
}
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);In Next.js, I can also use the built-in reportWebVitals function in app/layout.tsx or a custom _app.tsx. The library reports from real user sessions, which means I see performance on actual devices and networks, not just my development machine.
I check these metrics weekly on active projects. If LCP creeps above 2.5 seconds or INP above 200 milliseconds, I investigate and fix before the next deploy.
The fixes that usually matter most
The highest leverage fixes are often boring, which is good news. Compress and size images properly. Reserve space for media. Reduce render blocking work. Avoid shipping JavaScript that the page does not need on first view. Be careful with third party scripts. Keep component boundaries intentional. Those habits solve a surprising amount of pain.
I also look at content strategy. If the first viewport is trying to do too much, the design itself may be contributing to the problem. Cleaner composition can help performance and clarity at the same time.
Why scores without context can mislead
A Lighthouse score can point you toward issues, but it is not the whole story. Real user performance varies by device, network, and geography. That is why I care about what people actually experience, not just what one lab run reported on a fast machine.
I use the metrics as guidance, then ask whether the proposed fix improves the real journey. Some optimizations create complexity with little payoff. Others make the site noticeably better almost immediately.
Performance as a design discipline
Performance is not only an engineering concern. It is a design discipline too. Every extra module, visual treatment, and dependency has a cost. That does not mean design should become flat or timid. It means the experience should spend its budget intentionally.
I would rather build one strong visual idea that loads well than five competing elements that drag the whole page down. Good design and good performance can reinforce each other when the priorities are clear.
What I optimize for on client work
On client work, I optimize for pages that feel trustworthy, quick, and calm on the devices people actually use. That means balancing SEO, content richness, design quality, and code discipline instead of chasing a single vanity score.
If performance matters for your site, see my performance optimization service, browse the case studies, or hire me to improve both the numbers and the real user experience.