β‘ TL;DR: Your laptop lies about speed. Measure what real users feel with LCP, INP, and CLS, then spend effort on the few highest-leverage fixes that actually move those numbers instead of micro-optimizing things users never notice.
Contents
- The site that lied to you
- The principle: measure what users feel
- The page-load timeline, mapped to the user
- The three vitals at a glance
- Two fixes that pay for themselves
- Common mistakes that cost hours
- Takeaways
- Where to go next
The site that lied to you
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.
The one rule of performance work
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.
| In the real world | In tech |
|---|---|
| How fast the main dish actually arrives at your table | LCP, when the largest, most meaningful content appears |
| The waiter responding quickly when you ask for the bill | INP, how fast the page reacts to your interactions |
| Your table not wobbling and spilling your drink mid-meal | CLS, 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.
- Measure: Run Lighthouse in Chrome DevTools, but more importantly pull field data from the PageSpeed Insights 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.
- 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.
- 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.
- 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
<!-- 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
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"));
export function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<section>
<button onClick={() => setShowChart(true)}>Show analytics</button>
{showChart && (
<Suspense fallback={<p>Loading chartβ¦</p>}>
<Chart />
</Suspense>
)}
</section>
);
}
π‘ 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: swapwith a matched fallback.
Takeaways
The whole article in seven lines
- Performance is what users feel, on their devices, not what your laptop reports.
- Three vitals: LCP (loading, β€ 2.5 s), INP (interactivity, β€ 200 ms), CLS (stability, β€ 0.1).
- 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.
- Follow the full Frontend Engineer career path to put performance in context with the rest of the role.
- Re-read How the Browser Renders a Page, render-blocking resources and layout are the root of LCP and CLS.
- Use Frontend Build Tooling & Bundling to set up route-level code-splitting in your own project.
- Run your own site through PageSpeed Insights today and write down which of the three vitals is your worst, that is your first fix.
Originally published on TheSimplifiedTech, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.
Top comments (0)