Core Web Vitals: The Millisecond Economy
Google ranks you based on user experience. A technical deep dive into LCP (Loading), CLS (Stability), and INP (Interaction).
Google used to rank websites based on keywords and backlinks. In 2021, they introduced the Page Experience Update. They shifted the algorithm to filter out sites that are “Annoying”.
- Annoying = Slow (LCP).
- Annoying = Jumpy (CLS).
- Annoying = Unresponsive (INP).
If you fail these Core Web Vitals (CWV), you don’t just annoy users; you lose organic traffic. At Maison Code Paris, we treat Performance as a Feature. A slow luxury site is a contradiction in terms.
1. Largest Contentful Paint (LCP): The 2.5s Deadline
LCP measures Loading Performance. It stops the clock when the largest visible element (usually the Hero Image or H1 Blocking Text) is rendered. Target: < 2.5 seconds.
The Physics of LCP
LCP is a race against four sub-parts:
- TTFB (Time to First Byte): Server response time.
- Resource Load Delay: Time until browser discovers the image URL.
- Resource Load Time: Time to download the image.
- Element Render Delay: Time to paint pixels.
How to fix it (Next.js Strategy)
1. Don’t Lazy Load the Hero
Developers love loading="lazy". But if you put it on the Hero Image, the browser waits until it parses the <img> tag to start downloading.
You must Eager Load the LCP candidate.
// Bad
<Image src="/hero.jpg" loading="lazy" />
// Good (Next.js)
<Image src="/hero.jpg" priority={true} />
2. Preload Critical Resources
Tell the browser in the <head>: “You will need this font and this image immediately.”
<link rel="preload" href="/hero.webp" as="image" />
<link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin />
3. Use AVIF Format WebP is good. AVIF is better (20% smaller). Ensure your CDN supports content negotiation.
2. Cumulative Layout Shift (CLS): The Stability Index
CLS measures Visual Stability. It calculates how much content shifts unexpectedly. Target: < 0.1.
Scenario: User tries to click “Checkout”. An ad loads above the button. The content pushes down. The user clicks “Cancel” instead. This is a dark pattern, even if accidental.
The Problem: Aspect Ratios
If you include an image without dimensions:
<img src="photo.jpg" />
The browser doesn’t know the height until the image downloads. It renders 0px height, then jumps to 500px.
Fix: Always reserve space.
CSS aspect-ratio is your friend.
.hero-wrapper {
aspect-ratio: 16 / 9;
background: #f0f0f0; /* Placeholder color */
}
The Font Shift (FOIT/FOUT)
Custom fonts take time to load. If the browser hides text until the font loads (FOIT), you get a flash. If the browser shows fallback font (Arial) then swaps (Inter), the width changes. Line breaks change. Layout shifts.
Fix: Use font-display: optional or match metrics.
We use @next/font which automatically aligns fallback font metrics to prevent layout shift.
3. The Cost of Heavy Fonts
Designers love fonts. Engineers fear them. A single weight of Circular Std is 30KB (WOFF2). If you load Regular, Bold, Italic, Black -> 120KB. This blocks the text rendering (FOIT). Strategy:
- Subset: You don’t need Greek/Cyrillic characters. Subset to Latin-1. (Saves 50%).
- Variable Fonts: Use one file (
Inter-Variable.woff2) that contains all weights. It handles animation better. - Self-Host: Eliminate the TLS handshake to
fonts.googleapis.com.
4. Deferring Non-Critical CSS
Tailwind is great because it is small (10KB).
But old Sass projects often have main.css (500KB).
The browser halts rendering until main.css is downloaded.
Fix: Extract “Critical CSS” (styles for the header/hero) and inline it in <head>.
Load the rest asynchronously:
<link rel="stylesheet" href="main.css" media="print" onload="this.media='all'">
This trick boosts LCP by ~1 second on 3G networks.
5. Interaction to Next Paint (INP): The Responsiveness
Note: INP replaced FID (First Input Delay) in March 2024.
INP measures Event Latency. “When I click the Menu button, how many milliseconds until the Menu actually opens?” Target: < 200 milliseconds.
The culprit is almost always The Main Thread. JavaScript is single-threaded. If the browser is busy parsing a massive 5MB hydration bundle, and you click “Menu”, the browser says: “Wait, I’m busy.” The UI freezes.
Strategies to Fix INP
1. Yield to Main Thread Break up long tasks. If you are iterating over 10,000 items, do it in chunks.
// Blocking
items.forEach(process);
// Non-Blocking (Yielding)
async function processAsync() {
for (const item of items) {
await scheduler.postTask(() => process(item));
}
}
2. React useTransition
In React 18, use useTransition to mark updates as non-urgent.
This tells React: “If the user clicks, interrupt this rendering work to handle the click.”
const [isPending, startTransition] = useTransition();
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
4. Measuring: Field Data vs Lab Data
Developers live in Lab Data (Lighthouse on a fast MacBook). Google lives in Field Data (CrUX - Chrome User Experience Report). CrUX collects real data from real users (often on slow Android phones).
Your Lighthouse score might be 100. But if your P75 (75th Percentile) user has a slow connection, your CrUX score might be “Poor”. Always optimize for the P75 user on 4G.
5. Third Party Impact
Who is killing your Web Vitals? Usually:
- Chat Widgets (Intercom/Drift): Huge bundles.
- Tracking Pixels: Blocking main thread.
- A/B Testing Tools (Optimizely): Hiding body content for “flicker free” experience (kills LCP).
Solution: Move them to Web Workers using Partytown. (See Bundle Size).
6. The Speculation Rules API (Prerendering)
This is the nuclear option for speed. You can tell Chrome to render the next page in the background before the user clicks. Not just download the HTML. Execute the JS and Paint the Pixels. When they click, the transition is 0ms. Instant.
{
"prerender": [
{
"source": "list",
"urls": ["/next-page", "/cart"]
}
]
}
We use this for the “Add to Cart” -> “Checkout” flow. It feels like a native app.
7. Soft Navigations and CWV
In a Single Page App (Next.js), navigating from Home to Product Page is a Soft Navigation.
Previously, Core Web Vitals heavily ignored these.
In 2024, Google updated the metrics to track “INP on Soft Navigations”.
If your JS transition takes 500ms to fetch JSON and render the new page, you are failing LCP/INP.
Fix: Use Optimistic UI.
Show a Skeleton immediately. Do not wait for the await fetch() to complete before changing the URL.
9. Real User Monitoring (RUM)
Lighthouse is a “Lab Test”. It runs in a clean environment. Real users have Spotify running in the background and 50 tabs open. You need RUM. We use Vercel Analytics (or SpeedCurve). It tracks the Vitals for actual visitors. If your LCP is 1.2s on iPhone 15 but 4.5s on iPhone 11, RUM tells you. Lab data lies. Field data detects revenue leaks.
Why Maison Code Discusses This
At Maison Code, we are Performance Extremists. We don’t just “minify CSS”. We architect for Zero-Layout-Shift and Instant-Interaction. We know that for Luxury Brands, “Slow” = “Cheap”. We audit your entire stack, from the Edge CDN to the React Hydration. We deliver sites that feel native, even on 3rd world 4G networks. Because 100ms of latency = 1% revenue drop (Amazon Study).
12. The Difference Between TBT and INP
Total Blocking Time (TBT) is a Lab Metric. Interaction to Next Paint (INP) is a Field Metric. TBT predicts INP. If your TBT is high (>200ms), your INP will likely be poor. Optimization Loop:
- Run Lighthouse (measure TBT).
- Optimize hydration.
- Deploy.
- Wait 28 days for CrUX data (measure INP).
- Repeat. Don’t wait 28 days to know if you fixed it. Use TBT as your proxy.
13. Conclusion
Core Web Vitals are not just metrics. They are proxies for respect. Respecting your user’s time. Respecting their device battery. Google is simply enforcing good manners. If you treat the user well, Google treats you well.
Is your site slow?
Does your Search Console show “Poor URLs”? We conduct Deep Dive Performance Audits to fix Hydration hell.