How to Pass Core Web Vitals (INP & LCP) on Astro & Next.js Sites
Expert Byline
Sushmith Reddy
Release Date
August 03, 2026
I audit a lot of massive web applications. And I see the exact same tragedy play out every single week.
An engineering team decides to rebuild their company website. They ditch the old, clunky WordPress monolith and build a beautiful, modern application using Next.js or Astro. They deploy it on Vercel. They run a quick Lighthouse test on their local machine, see a green 99/100, and give each other high-fives.
Then, three weeks later, the marketing team opens Google Search Console and panics. The dashboard is bleeding red. Organic rankings are slipping. Google is throwing massive errors for INP and LCP.
The developers are confused. They search for "how to fix INP Core Web Vitals," but every blog post they find gives them outdated advice meant for 2015. They are told to "minify CSS" or "install an image optimizer plugin."
That advice is completely useless to you. You are building in modern JavaScript. You are running server-side rendering or static site generation. Your bottlenecks are entirely different.
Let's strip away the generic fluff. We are going to look at the actual, technical realities of passing Core Web Vitals using modern front-end frameworks. I am going to show you exactly how to improve LCP, fix your React hydration nightmares, and get your Next.js and Astro sites back in the green.
Quick Navigation Map:
The LCP Nightmare: How to Actually Improve LCP
LCP stands for Largest Contentful Paint. Google wants to know exactly how long it takes for the biggest element on your screen (usually a hero image or a massive H1 text block) to fully render. If it takes longer than 2.5 seconds on a 4G mobile connection, you fail.
Here is why most Next.js and React apps fail this test.
Stop Client-Side Rendering the Hero
If your hero section relies on a useEffect hook to fetch data before it displays an image, you have already lost.
Googlebot and real users do not want to download a blank HTML file, wait for 500kb of React to parse, and then wait for an API call just to see your banner image.
The Fix: You must fetch your initial above-the-fold data on the server. In the Next.js App Router, this means keeping your hero component as a default Server Component. In Astro, this happens automatically since the framework ships raw HTML by default. Serve the data instantly in the initial document request.
Mastering the Next.js Image Component
The Next.js <Image /> component is brilliant, but it is heavily misused. Out of the box, it lazy-loads images. This is great for pictures at the bottom of the page, but it is a death sentence if applied to your main hero image.
If you lazy-load your LCP element, the browser won't start downloading it until it has finished painting the rest of the layout.
The Fix: You need to explicitly tell the browser to prioritize the hero image.
import Image from 'next/image'
export default function HeroSection() {
return (
<Image
src="/hero-banner.jpg"
alt="Our amazing product"
width={1200}
height={800}
priority={true} // CRITICAL: Disables lazy loading
fetchPriority="high" // Tells the browser to grab this first
/>
)
}
By adding priority={true} and fetchPriority="high", you inject a <link rel="preload"> tag into the head of your document. The browser starts downloading the image the absolute microsecond it receives the HTML.
The Astro LCP Advantage
If you are building a highly visual, content-heavy site (like a blog or an e-commerce storefront), Astro makes passing LCP almost effortless.
Because Astro strips out unnecessary JavaScript by default, the main thread is never blocked during the initial page load. You just use the native <Picture /> or <Image /> components from astro:assets, set loading="eager" on your hero, and let the native browser caching do the heavy lifting.
Fixing INP Core Web Vitals (The Hydration Bottleneck)
In March 2024, Google replaced FID (First Input Delay) with INP (Interaction to Next Paint). This caused a massive panic in the React community.
INP measures the time between a user clicking something (like an accordion, a mobile menu, or an "Add to Cart" button) and the browser actually updating the screen to show the result.
If your INP is high, your site feels laggy, heavy, and broken.
Why React Hates INP
In a standard Next.js application, the server sends a beautiful HTML file. The page looks ready. The user taps a button. But nothing happens for 400 milliseconds.
Why? Because the browser is busy executing massive bundles of JavaScript. It is "hydrating" the DOM. React has locked up the browser's main thread, meaning the browser literally cannot respond to the user's click until React finishes doing its math.
To fix INP Core Web Vitals, you have to stop locking up the main thread.
The Next.js Fix: Aggressive Server Components
If you are using the Next.js App Router, you need to severely limit your use of the "use client" directive.
Every time you declare a Client Component, you are sending JavaScript to the browser. If you wrap your entire layout in a client-side state provider, you are forcing the browser to hydrate the entire page.
The Fix: Push all your non-interactive UI to the server. Keep your text, your layouts, and your static images as Server Components. Only drop the "use client" directive on the exact micro-components that need it (like the specific <LikeButton /> or the <SearchInput />). This drastically shrinks your JavaScript bundle, freeing up the main thread to handle user clicks instantly.
The Astro Fix: Islands Architecture
Astro was literally built to solve the INP problem. It uses a concept called "Islands Architecture."
By default, an Astro site ships zero kilobytes of JavaScript to the browser. The UI is completely static. But what if you need an interactive React carousel in the middle of the page? You create an "island."
You can tell Astro exactly when to load the heavy JavaScript for that specific component.
<Carousel client:load />(Loads the JS immediately. Bad for INP if it's heavy).<Carousel client:idle />(Loads the JS only after the main thread is totally free. Incredible for INP).<Carousel client:visible />(Loads the JS only when the user actually scrolls down and sees the component).
By heavily utilizing client:idle and client:visible, you keep the initial page load microscopically light. The browser is completely free to instantly respond to the user's first click.
Yielding to the Main Thread
Sometimes, you have to run heavy math on the client. Maybe you are filtering a massive list of products. If that filter function takes 200ms to run, it blocks the main thread, and your INP score tanks.
The Fix: Break up the heavy task. You need to "yield" to the main thread.
Instead of running a massive array filter all at once, you can use setTimeout or the modern scheduler.yield() API to pause your heavy function, let the browser quickly paint the button click state (like a loading spinner), and then resume the math.
// A simple example of breaking up a heavy task
async function handleFilterClick() {
setLoadingState(true); // Tell React to show a spinner
// Pause our heavy function to let the browser actually paint the spinner
await new Promise(resolve => setTimeout(resolve, 0));
// Now run the heavy filtering logic
const filteredData = performHeavyMath(data);
updateUI(filteredData);
}
Eliminating Layout Shifts (CLS)
Cumulative Layout Shift (CLS) is the third pillar. It measures how violently your page jumps around as things load.
In modern frameworks, this almost always comes down to two things: images without dimensions, and custom fonts.
1. Hardcode Image Dimensions
Never rely on CSS alone to size an image. Always provide explicit width and height attributes to your next/image or HTML img tags. This tells the browser exactly how much blank space to reserve on the page before the image even finishes downloading. If you do this, the layout will never shift.
2. Tame Your Custom Fonts
If you use a custom Google Font, the browser will render invisible text, or fallback text, until the custom font file downloads. When it finally swaps the fonts, the text size changes, and the whole page violently shifts down.
If you use Next.js, always use the next/font module. It automatically self-hosts your fonts and injects zero-layout-shift CSS wrappers.
// Next.js handles the CSS layout shift math automatically
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
)
}
The Real World Audit Process
Stop relying purely on the Lighthouse score running in your local Chrome browser. Your massive M3 MacBook Pro on gigabit fiber is going to execute React hydration infinitely faster than a three-year-old Android phone on a 3G subway connection.
To truly fix INP Core Web Vitals, you have to look at field data.
Use the Chrome User Experience Report (CrUX). This data lives directly inside your Google Search Console under the "Core Web Vitals" tab. It shows you exactly what real humans are experiencing on real devices.
If GSC says your INP is failing, but your local Lighthouse score is 100, trust GSC. Throttle your browser network to "Slow 4G" and throttle your CPU to "4x slowdown" in the Chrome DevTools. Click around your site. You will immediately feel the heavy, lagging buttons causing your INP failures.
The Bottom Line
You cannot brute-force your way into passing Core Web Vitals in 2026.
If you are using Next.js or Astro, you have incredible power at your fingertips, but you have to respect the browser's main thread. Stop forcing the client to do the server's job. Prioritize your hero images to fix LCP. Push your heavy JavaScript to the server (or delay it with Astro islands) to fix INP. Reserve the exact pixel dimensions for your assets to kill layout shifts.
Build the UI cleanly, respect the user's hardware, and watch your search rankings climb.