Your site flies on your laptop and crawls on a mid-range phone. Here is how to measure real user-perceived speed, LCP, INP, CLS, and the highest-leverage fixes that actually move the numbers.
You build a page. On your laptop it is instant, text snaps in, images pop, buttons respond the moment you click. You ship it. Then a user opens it on a three-year-old Android phone over patchy mobile data, and it is a different product entirely: a blank screen for two seconds, then a hero image that shoves the headline down just as they reach for it, then a tap that does nothing for half a second because the main thread is busy parsing your JavaScript.
Nothing changed about your code between those two experiences. What changed was the device and the network. Your laptop has a fast CPU, a fast connection, and a warm cache. Most of your users have none of those. The gap between "works on my machine" and "works for my users" is exactly the gap Core Web Vitals were invented to close.
Who this is for
Junior frontend engineers who can build a working page but have never deliberately measured its speed. If you have ever shipped something that felt fast to you and slow to everyone else, this is the article that explains why, and what to do about it.
The principle: measure what users feel
Measure what users actually feel, on the devices they actually use, not what your laptop tells you on a good day.
Performance is not a single number like "page weight" or "load time". It is a sequence of moments the user lives through: *Can I see it yet? Is it stable while I read? Does it respond when I touch it?* Core Web Vitals are three metrics, each one standing in for one of those felt moments. They are deliberately user-centric, they do not care how clever your build is, only what the human in front of the screen experiences.
How fast the main dish actually arrives at your tableLCP, when the largest, most meaningful content appears
The waiter responding quickly when you ask for the billINP, how fast the page reacts to your interactions
Your table not wobbling and spilling your drink mid-mealCLS, the layout staying put instead of jumping around
Loading a page is like being seated at a restaurant.
A restaurant where the food is fast, the staff are responsive, and the table is steady feels good even if you cannot name why. Get any one of the three wrong and the whole experience sours, exactly how a page with great LCP but a janky layout still feels broken.
The page-load timeline, mapped to the user
Here is one page load as the user lives it, left to right in time. Each vital attaches to a different moment of that journey, load, stability, and interactivity are not the same event and you cannot fix them with the same lever.
A single page load over time: each Core Web Vital maps to a distinct moment the user perceives.
Notice the order. LCP happens early (loading), CLS accumulates across the whole load (stability), and INP is measured every time the user interacts (responsiveness). You optimize them in roughly that sequence, but you only know which one to attack after you measure.
1
Measure
Run Lighthouse in Chrome DevTools, but more importantly pull **field data** from the [PageSpeed Insights](https://pagespeed.web.dev) report or the CrUX dashboard. Field data is real users on real devices, that is the truth. Lab data is a controlled simulation; useful for debugging, dangerous as your only source.
2
Find the biggest offender
Look at which of the three vitals is in the "poor" or "needs improvement" band. Do not optimize a green metric. In the Lighthouse report, expand the failing metric to see what element or script is responsible, it names the exact LCP element and the longest tasks blocking the main thread.
3
Fix one thing
Apply the highest-leverage change for that specific vital, an oversized hero image, a render-blocking bundle, an unsized embed. Change one variable so you can attribute the result.
4
Re-measure
Re-run the same measurement and confirm the number actually moved. Then repeat from step two on the next-worst metric. Performance work is a loop, not a one-time pass, and field data updates over 28 days, so real-world wins take time to show.
The three vitals at a glance
Each vital has published thresholds. Google grades the 75th percentile of your users, meaning three out of four real visits must hit the "good" bar before the metric counts as passing. Optimizing for the average hides your slowest users; the 75th percentile is what forces you to care about that mid-range phone.
Metric
What it measures
Good threshold
Top fixes
LCP (Largest Contentful Paint)
Loading, how long until the largest visible element (usually a hero image or headline block) is painted
≤ 2.5 s
Optimize/compress the LCP image, preload it, use a CDN, remove render-blocking CSS/JS, serve modern formats (WebP/AVIF)
INP (Interaction to Next Paint)
Interactivity, how quickly the page visually responds to taps, clicks, and key presses across the whole visit
≤ 200 ms
Break up long tasks, reduce main-thread JS, code-split, defer non-critical scripts, debounce expensive handlers
CLS (Cumulative Layout Shift)
Visual stability, how much content unexpectedly jumps around as the page loads
≤ 0.1
Set width/height on images & video, reserve space for ads/embeds, avoid injecting content above existing content, preload fonts
The three Core Web Vitals, their thresholds, and where to spend your effort.
The bands are the same shape for all three: good is the threshold above, needs improvement is the grey middle, and poor is anything past the second cutoff (LCP > 4 s, INP > 500 ms, CLS > 0.25). INP replaced the older FID metric in 2024, FID only measured the *first* interaction's delay, while INP watches every interaction, which is a far harsher and more honest test.
Two fixes that pay for themselves
The two highest-leverage techniques for a junior to reach for are lazy-loading offscreen images (helps LCP and total bytes) and code-splitting heavy components (helps INP by shrinking the JavaScript the main thread must parse upfront). Both are one-liners in modern stacks.
Lazy-load images you cannot see yet
An image below the fold does not need to load before the user scrolls to it. The native loading="lazy" attribute defers it for free. The critical detail: always set `width` and `height` so the browser reserves the space, this is what prevents the layout shift (CLS) when the image finally arrives. One important exception: your LCP image (the hero) should *not* be lazy, load it eagerly, even preload it.
below-the-fold.html
html
<!-- Hero image: the LCP element. Load it eagerly + hint high priority. -->
<img
src="/hero.avif"
width="1200"
height="600"
fetchpriority="high"
alt="Product dashboard"
/>
<!-- Offscreen images: defer until the user scrolls near them. -->
<!-- width + height reserve the box so nothing jumps (protects CLS). -->
<img
src="/screenshot.avif"
width="800"
height="450"
loading="lazy"
alt="Feature screenshot"
/>
Code-split so the main thread can breathe
A 500 KB JavaScript bundle has to be downloaded, parsed, and executed before the page is interactive, and parsing JS is one of the most expensive things a phone's CPU does. If a heavy component (a chart library, a rich editor, a modal) is not needed on first paint, load it on demand with a dynamic import. The bundler splits it into a separate chunk that only ships when it is actually used.
Dashboard.tsx
tsx
import { lazy, Suspense, useState } from"react";
// Heavy charting lib is NOT in the initial bundle.// It downloads only when <Chart /> first renders.const Chart = lazy(() => import("./HeavyChart"));
exportfunctionDashboard() {
const [showChart, setShowChart] = useState(false);
return (
<section>
<button onClick={() => setShowChart(true)}>Show analytics</button>
{showChart && (
<Suspense fallback={<p>Loading chart…</p>}>
<Chart />
</Suspense>
)}
</section>
);
}
Pro tip
Code-splitting is not just for big libraries. Route-level splitting, loading each page's code only when the user navigates to it, is often the single biggest INP and LCP win, and most frameworks (Next.js, React Router) do it for you automatically once you stop importing everything into one shared bundle.
Common mistakes that cost hours
Optimizing the wrong thing. Spending a day shaving 50 ms off an already-green LCP while INP sits in the red. Always fix the worst metric first, measure before you touch anything.
Shipping huge JavaScript bundles. Importing an entire library to use one function, or bundling every route's code into one file. This is the number-one cause of bad INP on real devices, your laptop hides it, the phone does not.
Unsized images and embeds causing CLS. An <img> or ad iframe with no width/height reserves zero space, so content reflows and jumps when it loads. Cheap to fix, and a top source of "why does the page keep moving" complaints.
Lab-only testing. Trusting a single Lighthouse run on your fast laptop and declaring victory. Lab data is for debugging; field data (real users, 75th percentile) is the score that ships. Always check both.
Ignoring fonts. A web font that swaps in late pushes text around (CLS) or hides it entirely (delayed LCP). Preload critical fonts and use font-display: swap with a matched fallback.
Takeaways
The whole article in seven lines
Performance is what users *feel*, on *their* devices, not what your laptop reports.
Always measure with **field data** at the 75th percentile, not just a lab run.
Loop: measure → find the worst metric → fix one thing → re-measure → repeat.
Lazy-load offscreen images (but eager-load the LCP hero) and always set width/height.
Code-split heavy components with dynamic imports so the main thread parses less JS.
Most CLS bugs are unsized images, embeds, and late fonts, reserve the space.
Where to go next
Core Web Vitals make the most sense once you understand *why* the browser behaves the way it does. If you have not yet, read How the Browser Renders a Page, the render path is exactly what LCP and CLS are measuring. Then Frontend Build Tooling & Bundling shows you the mechanics behind code-splitting and shrinking the bundles that wreck INP.
Run your own site through PageSpeed Insights today and write down which of the three vitals is your worst, that is your first fix.
Want to go deeper?
This article covers concepts taught hands-on in the Cloud Engineer and DevOps career paths, with real terminal labs, production scenarios, and structured lessons.