Website performance isn’t just about speed anymore—it’s about delivering a seamless user experience. Google’s Core Web Vitals have become the standard metrics for measuring real-world performance, directly influencing search engine rankings and user satisfaction.
The three key Core Web Vitals are:
-
Largest Contentful Paint (LCP): Measures loading performance. Aim for <2.5s.
-
Interaction to Next Paint (INP, replacing FID): Measures responsiveness. Aim for <200ms.
-
Cumulative Layout Shift (CLS): Measures visual stability. Aim for <0.1.
While Next.js comes with many performance optimizations out of the box—such as image optimization, automatic code splitting, and server-side rendering—it doesn’t guarantee perfect scores. Developers need to apply best practices and fine-tuning to ensure Core Web Vitals remain in the green zone.
In this tutorial, you’ll learn how to analyze and improve Core Web Vitals in Next.js apps using practical techniques, including:
-
Measuring Core Web Vitals with built-in and external tools.
-
Optimizing images and fonts.
-
Enhancing page loading and interactivity.
-
Preventing layout shifts with stable UI patterns.
-
Leveraging Next.js features like dynamic imports and caching strategies.
By the end, you’ll have a performance-ready Next.js app that not only loads fast but also keeps users engaged and search engines happy.
Measuring Core Web Vitals in Next.js
Before improving performance, you need to measure it. Next.js makes it simple to collect Core Web Vitals metrics and analyze how your application performs in real-world conditions. Here are the most common approaches:
1. Using Google Lighthouse
Lighthouse is built into Chrome DevTools and provides an easy way to measure performance locally.
-
Open your app in Chrome.
-
Right-click → Inspect → go to the Lighthouse tab.
-
Run an audit for Performance.
-
Review your scores for LCP, INP, and CLS along with recommendations.
This gives you an overview, but it only reflects your local environment—not real users.
2. Using Next.js reportWebVitals
API
Next.js includes a built-in way to measure Core Web Vitals directly from your users’ browsers.
Create a pages/_app.tsx
(or update if it already exists):
// pages/_app.tsx
import type { AppProps } from 'next/app'
import type { NextWebVitalsMetric } from 'next/app'
export function reportWebVitals(metric: NextWebVitalsMetric) {
switch (metric.name) {
case 'LCP':
console.log('Largest Contentful Paint:', metric.value)
break
case 'FID': // or INP depending on browser support
console.log('First Input Delay / Interaction to Next Paint:', metric.value)
break
case 'CLS':
console.log('Cumulative Layout Shift:', metric.value)
break
default:
console.log(metric.name, metric.value)
}
}
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
When users interact with your app, metrics will be logged in the console. You can also send this data to an analytics service (e.g., Google Analytics, LogRocket, or your own API).
3. Using the Web Vitals Library
The web-vitals package offers more control and supports custom reporting.
Install it:
npm install web-vitals
Create a utility file, e.g. lib/vitals.ts
:
import { onCLS, onINP, onLCP } from 'web-vitals'
export function sendToAnalytics(metric: any) {
// Send data to your backend, GA, or logging service
console.log(metric.name, metric.value)
}
export function reportWebVitals() {
onCLS(sendToAnalytics)
onINP(sendToAnalytics)
onLCP(sendToAnalytics)
}
Then, initialize it inside _app.tsx
:
import type { AppProps } from 'next/app'
import { reportWebVitals } from '../lib/vitals'
if (typeof window !== 'undefined') {
reportWebVitals()
}
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
4. Using Field Data (Google Search Console + PageSpeed Insights)
Finally, you can get real-world Core Web Vitals data from Google’s tools:
-
Search Console: Monitors Core Web Vitals for your entire site.
-
PageSpeed Insights: Shows both lab (simulated) and field (real user) data.
This combination ensures you’re not just optimizing for local tests, but for actual users across devices and networks.
✅ Now we know how to measure Core Web Vitals in Next.js apps, the next step is learning how to improve them.
Optimizing Images and Fonts
Images and fonts are among the largest assets loaded on a Next.js page. If not optimized properly, they can delay rendering, hurt Largest Contentful Paint (LCP), and cause Cumulative Layout Shift (CLS). Next.js provides built-in tools to handle both efficiently.
1. Optimizing Images with next/image
Instead of using a regular <img>
tag, Next.js provides the Image
component, which automatically:
-
Serves images in modern formats (like WebP) when supported.
-
Resizes images for different devices (responsive).
-
Uses lazy loading for below-the-fold content.
-
Sets explicit
width
andheight
to prevent layout shifts.
Example:
// pages/index.tsx
import Image from 'next/image'
export default function Home() {
return (
<div>
<h1>Welcome to My Next.js App</h1>
<Image
src="/hero-banner.jpg"
alt="Hero Banner"
width={1200}
height={600}
priority // preload above-the-fold images
/>
</div>
)
}
🔑 Key tips:
-
Use the
priority
prop for above-the-fold images (e.g., hero banners, logos). -
Always specify
width
andheight
(orfill
with a container) to avoid CLS. -
Store static images in
/public
or use a CDN for remote images.
2. Optimizing Fonts
Fonts can block rendering if not loaded properly. Next.js introduced next/font
to optimize fonts automatically.
Example using Google Fonts:
// pages/_app.tsx
import type { AppProps } from 'next/app'
import { Roboto } from 'next/font/google'
const roboto = Roboto({
subsets: ['latin'],
weight: ['400', '700'],
display: 'swap', // prevents blocking render
})
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<main className={roboto.className}>
<Component {...pageProps} />
</main>
)
}
Benefits:
-
Fonts are self-hosted by default (no external requests to Google Fonts).
-
Automatic subset selection (only loads needed characters).
-
Uses
font-display: swap
to prevent invisible text during loading (reduces CLS).
3. Preloading Critical Assets
For fonts and above-the-fold images, consider preloading. Next.js automatically preloads Google Fonts with next/font
. For images, the priority
prop handles this.
For external fonts or assets, you can manually preload in _document.tsx
:
// pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Head>
<link
rel="preload"
href="/fonts/MyCustomFont.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
✅ With images and fonts optimized, you’ll see major improvements in LCP and fewer layout shifts.
Improving Page Loading and Interactivity
Fast loading alone isn’t enough—your app also needs to respond quickly to user input. Google’s Interaction to Next Paint (INP) metric measures how long it takes for your app to react to clicks, taps, and keyboard input. If your JavaScript is too heavy or blocking, INP (or older FID) scores will suffer.
Here are strategies to improve both loading speed and interactivity in Next.js apps:
1. Minimize JavaScript Payload
Large JavaScript bundles slow down initial load and delay interactivity. Next.js helps by:
-
Automatic Code Splitting – Each page only loads the JavaScript it needs.
-
Dynamic Imports – Load components only when they’re needed.
Example:
// pages/index.tsx
import dynamic from 'next/dynamic'
// Load a heavy chart only when needed
const Chart = dynamic(() => import('../components/Chart'), {
ssr: false, // only render on client
loading: () => <p>Loading chart...</p>,
})
export default function Home() {
return (
<div>
<h1>Dashboard</h1>
<Chart />
</div>
)
}
This prevents heavy components from blocking the first render.
2. Optimize Script Loading
Next.js provides the <Script>
component to fine-tune when third-party scripts are loaded:
import Script from 'next/script'
export default function Page() {
return (
<>
<h1>My Page</h1>
<Script
src="https://example.com/analytics.js"
strategy="lazyOnload" // load after page is interactive
/>
</>
)
}
Available strategies:
-
beforeInteractive
→ Load early (critical scripts). -
afterInteractive
→ Default, load after hydration. -
lazyOnload
→ Load during browser idle time.
Always prefer lazyOnload
for non-critical third-party scripts like analytics, ads, or chat widgets.
3. Optimize React Hydration
React apps need hydration (attaching event listeners) before becoming interactive. Some optimizations include:
-
Static Site Generation (SSG): Use
getStaticProps
when possible to serve pre-rendered pages. -
Partial Hydration: Consider frameworks/libraries like React Server Components (already supported in Next.js App Router).
-
Reduce client-side state: Keep as much logic as possible on the server.
4. Use Server Components (Next.js App Router)
If you’re using the App Router (app/
directory), you can take advantage of React Server Components (RSC), which render on the server and reduce JavaScript sent to the client.
// app/page.tsx
export default function Page() {
// This is a server component by default
return <h1>Hello, Server Components!</h1>
}
This means less client-side JS → faster interactivity.
5. Prefetching and Caching
Next.js automatically prefetches linked pages with the <Link>
component. Ensure caching headers are properly set for static assets and APIs.
import Link from 'next/link'
export default function Nav() {
return (
<nav>
<Link href="/about" prefetch>
About
</Link>
</nav>
)
}
This makes navigation almost instant.
✅ With optimized JavaScript, smart script loading, and server-side rendering, your Next.js app will feel more responsive and improve INP/FID scores significantly.
Preventing Layout Shifts (CLS)
Cumulative Layout Shift (CLS) measures visual stability—how much elements on a page unexpectedly move during load. High CLS scores frustrate users because they might click on the wrong button or lose their reading position.
Next.js helps avoid layout shifts, but you still need to follow best practices to keep your UI stable.
1. Always Define Image and Video Dimensions
Unexpected image resizing is the most common cause of layout shifts. With Next.js Image
, you can prevent this by specifying width
and height
(or using fill
inside a container with fixed dimensions.
import Image from 'next/image'
export default function Hero() {
return (
<div className="hero">
<Image
src="/banner.jpg"
alt="Hero Banner"
width={1200}
height={500}
priority
/>
</div>
)
}
✅ This ensures the browser reserves space before the image loads.
2. Reserve Space for Dynamic Content
Content injected by ads, iframes, or user actions can push elements around. Always reserve space with a fixed height or container box.
<div style={{ minHeight: '250px' }}>
{/* Ad or dynamic content goes here */}
</div>
If using third-party scripts, load them below the fold or after interaction when possible.
3. Avoid Flash of Invisible Text (FOIT)
Fonts can cause text re-rendering once they load. Next.js next/font
defaults to font-display: swap
, which shows fallback text immediately, avoiding shifts.
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], display: 'swap' })
✅ This ensures stable text rendering.
4. Keep Animations and Transitions Intentional
Avoid animations that move content unexpectedly. If animating, use transform
and opacity
instead of margin
or top
, since they don’t cause reflow.
/* Good: smooth fade without layout shift */
.fade-in {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
5. Stabilize Component Loading
If components like navigation bars, banners, or carousels load asynchronously, wrap them in placeholders or skeleton loaders to preserve layout.
export default function CardSkeleton() {
return (
<div style={{ width: '300px', height: '200px', background: '#eee' }} />
)
}
This avoids sudden jumps when real content replaces the placeholder.
✅ By defining dimensions, reserving space for dynamic elements, optimizing fonts, and using skeletons, you can keep your Next.js app’s CLS score below 0.1 for a smooth, stable experience.
Leveraging Next.js Features for Performance
Next.js provides a powerful toolkit that can dramatically improve Core Web Vitals when used correctly. Beyond images, fonts, and JavaScript optimizations, you can tap into caching, Incremental Static Regeneration (ISR), and middleware to boost performance across your app.
1. Static Site Generation (SSG)
Pages built with getStaticProps
are pre-rendered at build time and served as static HTML. This reduces server processing and ensures super-fast LCP.
// pages/index.tsx
export async function getStaticProps() {
const data = await fetch('https://api.example.com/posts').then(res => res.json())
return { props: { data } }
}
export default function Home({ data }: { data: any[] }) {
return (
<div>
<h1>Latest Posts</h1>
<ul>
{data.map((post, i) => <li key={i}>{post.title}</li>)}
</ul>
</div>
)
}
✅ Best for content that doesn’t change often.
2. Incremental Static Regeneration (ISR)
With ISR, you can serve static pages but revalidate them at a given interval. This keeps content fresh without rebuilding the whole site.
// pages/blog/[id].tsx
export async function getStaticProps({ params }: any) {
const post = await fetch(`https://api.example.com/posts/${params.id}`).then(res => res.json())
return {
props: { post },
revalidate: 60, // re-generate every 60s
}
}
✅ Improves LCP by serving cached static HTML while still keeping data up to date.
3. Caching with Headers
Next.js lets you set custom caching strategies for pages and API routes using response headers.
// pages/api/data.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.setHeader('Cache-Control', 's-maxage=300, stale-while-revalidate')
res.status(200).json({ time: Date.now() })
}
-
s-maxage=300
→ cache at the CDN (Vercel Edge) for 5 minutes. -
stale-while-revalidate
→ serve cached version while fetching a fresh one.
✅ Reduces API latency and improves perceived performance.
4. Middleware for Edge Performance
Next.js Middleware runs before a request is completed and allows fast, personalized responses at the edge.
Example: Redirecting users based on geolocation or authentication without hitting the server.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const url = request.nextUrl.clone()
if (!request.cookies.get('auth')) {
url.pathname = '/login'
return NextResponse.redirect(url)
}
return NextResponse.next()
}
✅ Improves responsiveness (INP) by reducing round-trips to the server.
5. Prefetching with <Link>
By default, Next.js <Link>
prefetches pages in the background, so when users click, navigation feels instant.
import Link from 'next/link'
export default function Nav() {
return (
<nav>
<Link href="/about" prefetch>About</Link>
<Link href="/blog" prefetch>Blog</Link>
</nav>
)
}
✅ Helps INP by making navigation lightning-fast.
6. Using the Next.js App Router (Server Components + Streaming)
The new App Router (app/
) takes performance further with:
-
React Server Components (RSC): Less client-side JavaScript.
-
Streaming + Suspense: Content loads progressively, improving perceived speed.
// app/page.tsx
export default async function Page() {
const data = await fetch('https://api.example.com/posts').then(r => r.json())
return (
<div>
<h1>Server Rendered Page</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
}
✅ This reduces JS bundle size and improves LCP + INP.
By combining SSG, ISR, caching, middleware, and prefetching, your Next.js app can deliver blazing-fast performance for both first-time visitors and returning users.
Best Practices for Sustaining Good Core Web Vitals
Improving Core Web Vitals isn’t a one-time task—it requires continuous monitoring and adherence to best practices throughout development. Here are some guidelines to keep your Next.js app consistently fast and stable:
1. Optimize Critical Rendering Path
-
Use
next/image
for responsive and optimized images. -
Load fonts with
next/font
(self-hosted by default). -
Preload above-the-fold assets using
priority
and prefetch links.
2. Keep JavaScript Lightweight
-
Use dynamic imports for heavy components.
-
Prefer Server Components (App Router) for less client-side JS.
-
Minimize third-party scripts—each one can hurt INP.
3. Improve Server-Side Delivery
-
Use Static Site Generation (SSG) and Incremental Static Regeneration (ISR) for predictable, cacheable content.
-
Apply proper caching headers (
s-maxage
,stale-while-revalidate
). -
Deploy on a CDN/Edge network (Vercel, Netlify, Cloudflare) for global low latency.
4. Prevent Layout Instability (CLS)
-
Always define the width/height for images and videos.
-
Reserve space for ads, iframes, and dynamic widgets.
-
Use skeleton loaders or placeholders for async content.
5. Monitor Core Web Vitals Continuously
-
Use Next.js
reportWebVitals
API to send metrics to analytics. -
Track real user monitoring (RUM) with tools like Google Analytics, LogRocket, or Sentry.
-
Regularly audit with Lighthouse and PageSpeed Insights.
6. Automate Performance Checks
-
Include performance budgets in your CI/CD pipeline.
-
Fail builds occur if JavaScript bundles or LCP/CLS/INP metrics exceed their respective thresholds.
✅ By following these best practices, you’ll ensure that your Next.js app remains fast, stable, and responsive even as it grows in complexity.
Conclusion
Optimizing Core Web Vitals in a Next.js app is about more than just speed—it’s about delivering a smooth, engaging experience for your users. By measuring metrics with Lighthouse, reportWebVitals
, and real-user data, you gain visibility into performance bottlenecks. Then, through targeted improvements—like optimizing images and fonts, reducing JavaScript payloads, preventing layout shifts, and leveraging Next.js features such as SSG, ISR, caching, and middleware—you can achieve consistent green scores across LCP, INP, and CLS.
The key takeaway: performance is an ongoing process. Regular monitoring, adopting best practices, and making performance-conscious development decisions will ensure your Next.js applications stay fast, user-friendly, and SEO-friendly.
With these strategies, you’ll not only improve Core Web Vitals but also gain happier users, higher engagement, and better rankings in search results.
You can find the full source code on our GitHub.
That's just the basics. If you need more deep learning about Next.js, you can take the following cheap course:
- Next.js 15 & React - The Complete Guide
- The Ultimate React Course 2025: React, Next.js, Redux & More
- React - The Complete Guide 2025 (incl. Next.js, Redux)
- Next.js Ecommerce 2025 - Shopping Platform From Scratch
- Next JS: The Complete Developer's Guide
- Next.js & Supabase Mastery: Build 2 Full-Stack Apps
- Complete React, Next.js & TypeScript Projects Course 2025
- Next.js 15 Full Stack Complete Learning Management System
- Next.js 15 & Firebase
- Master Next.js 15 - Build and Deploy an E-Commerce Project
Thanks!