Optimizing Core Web Vitals (LCP, FID, CLS) in Next.js
Building a fast, user-friendly web application is critical for both user experience (UX) and search engine optimization (SEO). Google’s Core Web Vitals, a set of key performance metrics, have become a crucial ranking factor, measuring how your site performs in terms of loading speed, interactivity, and visual stability. In this post, we’ll explain each Core Web Vital (Largest Contentful Paint, First Input Delay, and Cumulative Layout Shift), why it matters, and provide actionable Next.js-specific strategies (with code examples) to optimize each metric. We’ll also cover do’s and don’ts for Next.js development and how to measure your Core Web Vitals using tools like Lighthouse, Chrome DevTools, GTmetrix, WebPageTest, and field data from CrUX and PageSpeed Insights.
What Are Core Web Vitals?¶
Core Web Vitals consist of three primary metrics that Google considers important for user experience:
- Largest Contentful Paint (LCP): Measures loading performance, specifically, how long it takes for the largest content element (image or text block) in the viewport to render. A good LCP is 2.5 seconds or less (at the 75th percentile of users).
- First Input Delay (FID): Measures interactivity, the delay between a user’s first interaction (e.g. click or tap) and the browser’s response. A good FID is under 100 milliseconds.
- Cumulative Layout Shift (CLS): Measures visual stability, how much the page’s layout shifts unexpectedly. A good CLS score is 0.1 or less.
Google uses these metrics to gauge page experience and influence search rankings. In other words, a Next.js site that scores well on LCP, FID, and CLS is more likely to keep users engaged and rank higher on Google.
Below, we’ll dive into each metric and explore Next.js-focused techniques to improve them.
Largest Contentful Paint (LCP)¶
Largest Contentful Paint is the time it takes for the main content of the page to load, usually the biggest image or text visible in the viewport. It essentially measures loading speed: a slow LCP means users may stare at a blank or half-rendered page, leading to frustration or abandonment. Google recommends an LCP of 2.5s or faster for most users.
Why LCP Matters: A fast LCP means users see content quickly. This improves user engagement and reduces bounce rates, and it’s rewarded by Google’s ranking algorithm. A poor LCP (over 4 seconds) can hurt your SEO and make users likely to leave.
Next.js Strategies to Optimize LCP¶
Next.js offers several features to help achieve a fast LCP:
Optimize Images with next/image
: Large images are a common LCP element. Next.js’s built-in Image component automatically serves optimized images (resized, compressed, and in modern formats like WebP) which load faster. It also sets width/height to avoid layout shifts and lazy-loads offscreen images for efficiency. Use next/image
for hero banners, above-the-fold graphics, and mark the most important image as priority
to preload it:
import Image from 'next/image';
export default function Home() {
return (
<header>
<h1>My Site</h1>
<Image
src="/images/hero.jpg"
alt="Hero image"
width={1200}
height={800}
priority={true} // Preload this image for faster LCP
placeholder="blur" // Optional blur-up placeholder
/>
</header>
);
}
Do: Use the priority
prop for the LCP image (typically your above-the-fold image) so that Next.js preloads it for quicker rendering. Don’t: Use raw <img>
tags for large images, you’ll miss out on optimization. Also avoid massive image files; always compress using lossless compression and size images appropriately.
Preload Critical Assets: Aside from images, other assets can affect LCP (like critical CSS or web fonts). Next.js allows adding custom <Head>
tags in your pages or _document.js
. Preload your main CSS or important font files so the browser downloads them early:
// pages/_document.js (for Next.js Pages Router)
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html>
<Head>
{/* Preload a critical font */}
<link rel="preload" href="/fonts/my-font.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />
{/* Preload main CSS */}
<link rel="preload" href="/css/main.css" as="style" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
Preloading ensures these resources are fetched quickly, reducing the time to render your largest content. Do: Preload the assets that are essential for above-the-fold content (fonts, critical CSS, hero images). Don’t: Overuse preload on every resource, focus only on those that improve initial render, as preloading too much can backfire by consuming bandwidth early.
Use Static Generation and Caching: Slow server responses delay LCP. Next.js can pre-render pages at build time (SSG) or use Incremental Static Regeneration, so pages are served as static files via CDN, drastically reducing Time to First Byte. For dynamic data, consider caching or using the Next.js Edge Network (Vercel Edge) to speed up delivery. Do: Use getStaticProps
for pages that don’t need per-request data, and enable caching headers for static assets. Don’t: Put long blocking computations in API routes or getServerSideProps
for critical pages, that will slow the HTML delivery and hurt LCP.
Minimize Render-Blocking JS/CSS: Eliminate or defer any scripts and styles that aren’t needed for initial paint. Next.js automatically code-splits by route, but you can further split large components. We’ll cover dynamic imports (for JS) in the FID section, which also helps LCP by reducing bundle size. For CSS, leverage global styles or CSS modules that are scoped; avoid huge CSS files if not necessary. Do: Remove unused CSS/JS (e.g., strip out dead code, use tree-shaking). Don’t: Import large libraries on the client if you can load them conditionally or server-side.
Leverage Next.js Image placeholders: Using placeholder="blur"
with a small blurDataURL
(which Next can generate for static images) can improve perceived LCP. Users see a blurred preview before the full image loads, making the page appear to load faster.
LCP Optimization Do’s and Don’ts (Summary):
- Do: Use optimized formats (WebP/AVIF) and the Next.js Image component for large visuals. Preload the hero image with
priority
. Serve pages statically via CDN when possible, and optimize server response times. - Do: Inline or preload critical CSS and font files to avoid delays.
- Don’t: Use unoptimized images or skip specifying image dimensions. Don’t load huge JS bundles on the initial page, split code or defer scripts so they don’t delay the largest content rendering.
First Input Delay (FID)¶
First Input Delay measures how quickly your page responds to the first user interaction (like clicking a link or tapping a button). It specifically tracks the delay from the user’s action to the browser processing that event. Unlike LCP and CLS, FID is a field metric (measured with real users), it can’t be simulated in lab tools easily. However, a related lab metric Total Blocking Time (TBT) is often used as a proxy (a high TBT usually means poor FID). Google’s recommendation is to keep FID below 100 ms for 75% of users, which means the page feels nearly instant to interact.
Why FID Matters: Users expect immediate interactivity. If a page is slow to respond (even if it looks loaded), it frustrates users, e.g., tapping a button and nothing happens, often because the browser’s main thread is busy. A poor FID (hundreds of milliseconds delay) can cause users to tap repeatedly or abandon the interaction. Improving FID makes your app feel responsive and is rewarded in search rankings.
Next.js Strategies to Optimize FID¶
FID issues are usually caused by heavy JavaScript execution blocking the main thread. Next.js and modern tooling can mitigate this:
Code-Splitting with Dynamic Imports: Splitting your JavaScript bundle means users download and execute less code upfront, reducing the chance of long main-thread blocks. Next.js supports dynamic import for components. For example, if you have a heavy component or library not needed immediately, you can load it dynamically:
import dynamic from 'next/dynamic';
// Dynamically import a heavy component (client-side only, no SSR)
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
ssr: false, // don't include in server-side render (reduces initial JS)
loading: () => <p>Loading...</p>, // fallback during load
});
function HomePage() {
return (
<div>
<h1>Welcome</h1>
{/* HeavyComponent will load only when HomePage is rendered on client */}
<HeavyComponent />
</div>
);
}
By using dynamic()
with ssr: false
, this large component’s code is not part of the initial JS bundle, improving initial load and interactivity. Do: Use dynamic import for charts, maps, or any module that isn’t immediately needed on page load. Don’t: Dynamic-import something that’s required for above-the-fold content (that could harm LCP), use it primarily for below-the-fold or on-demand features.
Deferring Non-Critical Scripts with next/script
: Third-party scripts (analytics, ads, widgets) can significantly delay interactivity if loaded improperly. Next.js provides the <Script>
component to control when external scripts execute. You can assign a loading strategy:
afterInteractive
(default), load script after the page is interactive.lazyOnload,
load when browser is idle (great for things like analytics).beforeInteractive,
(use sparingly) injects script early, often in_document
, if absolutely needed for initial UI.
For example, to load Google Analytics after page load:
import Script from 'next/script';
export default function MyPage() {
return (
<>
{/* ... page content ... */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=GA_TRACKING_ID"
strategy="lazyOnload"
/>
<Script id="ga-init" strategy="lazyOnload">
{`window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);}
gtag('js', new Date()); gtag('config', 'GA_TRACKING_ID');`}
</Script>
</>
);
}
Here we use strategy="lazyOnload"
so that analytics loading waits until after initial page load, ensuring it doesn’t block user interactions. Do: Load third-party scripts with Next’s Script strategies to avoid main-thread blocking. Use worker
strategy (with Partytown) if you have heavy scripts that can run in a web worker thread, keeping the main thread free. Don’t: Inline large third-party script tags in your page head or body, as they will execute immediately and potentially delay user interactivity.
Minimize JavaScript and Work Efficiently: Next.js automatically tree-shakes and minifies production JS, but you should still avoid heavy computation on the main thread. If you have expensive calculations, consider moving them to a Web Worker or computing on the server side. Also ensure you’re not rendering large lists or doing heavy state updates right at startup. Use React’s performance features (like memoization, or in Next 13’s App Router, React Server Components to offload work to the server). Do: Profile your app, identify long tasks (>50ms) that cause blocking and refactor them. Breaking up long tasks or using requestIdleCallback
for low-priority work can help keep FID low. Don’t: Load unnecessary polyfills or large libraries on older browsers unless needed (Next.js can automatically polyfill only what’s needed).
Leverage Next.js SSR vs CSR wisely: Next.js can server-render pages, but remember that after SSR, the JavaScript on the client still has to hydrate the page. A very complex page will have a lot of JS to execute during hydration, which can delay interactivity. If you find hydration is heavy, consider using static generation (no heavy data fetching at runtime) or splitting the page into simpler components that hydrate independently. Next 13’s React Server Components model (App Router) can also help by reducing the amount of client JS needed, thus improving FID. The key is to ship less JS to the browser whenever possible.
FID Optimization Do’s and Don’ts:
- Do: Split your code, only deliver essential code for the initial view. Utilize Next.js dynamic imports to delay loading of heavy components until needed.
- Do:Defer third-party scripts using
next/script
withafterInteractive
orlazyOnload
so they don’t block your app’s own code. Consider usingstrategy="worker"
(with Partytown) for scripts that can run off the main thread. - Don’t: Block the main thread with long JavaScript execution during page load. Don’t load analytics, ads, or trackers synchronously before your page becomes interactive.
- Don’t: Neglect performance in development, test your Next.js app with Lighthouse or Chrome DevTools Performance panel to catch high TBT or long tasks that would hurt FID.
Cumulative Layout Shift (CLS)¶
Cumulative Layout Shift measures the sum of all unexpected layout shifts that occur while a page is open. In simple terms, CLS quantifies how much things jump around on screen. A layout shift happens when elements move from their initial position, e.g., an image loading pushes text down, or an ad banner injects and shoves content. If these shifts happen without user input and disturb the page’s stability, they contribute to CLS. Google considers a CLS score ≤ 0.1 as good, and ≥ 0.25 as poor.
Why CLS Matters: A low CLS means a stable page, which is less frustrating. High CLS can cause misclicks (clicking the wrong button because it moved) and makes the site feel janky. For example, imagine reading an article and suddenly a late-loading image causes the text to shift, you lose your place. Such poor experiences can drive users away. Moreover, Google prioritizes pages with stable layouts in search rankings, since it indicates better UX.
Next.js Strategies to Optimize CLS¶
Most layout shifts can be prevented by careful development practices, and Next.js tools make this easier:
Always Include Size Attributes for Images and Media: A top culprit for CLS is images without dimensions, the browser doesn’t know how much space to reserve, so content jumps when the image loads. Next.js’s next/image
automatically adds width and height (for static imports) or requires you to specify them for external images, ensuring the browser reserves the correct space. Always use the Image component or set explicit width
/height
on your <img>
/<video>
/<iframe>
tags. This way, no sudden shift occurs when media loads. Do: Use Next Image for responsive images, it even provides a blur placeholder to smooth the appearance without shifting content. Don’t: Insert images via CSS background without accounting for their container height, and don’t forget to set dimensions on <canvas>
or other elements that might render later.
Reserve Space for Dynamic Content: If you plan to load ads, embeds, or any component asynchronously, design the layout to have a container of fixed size or min-height for that content. For example, if you show a cookie consent banner dynamically, define a container that doesn’t collapse so when it appears it doesn’t push the page. Do: Use CSS to create placeholders or reserve min-heights for components that load late. Don’t: Let content pop in above existing content without a reserved space.
Use Next.js Font Optimization: Flash of unstyled text or shifting text can also contribute to CLS (if a fallback font swaps to a custom font and the size changes). Next.js provides an integrated font optimization (the next/font
library) to load fonts efficiently. For Google Fonts, you can use the built-in font loader which automatically adds font-display: swap
(so text is immediately rendered with a fallback font, then switches to the custom font without invisible text). For example:
import { Roboto } from 'next/font/google';
const roboto = Roboto({
subsets: ['latin'],
weight: '400',
display: 'swap', // ensures fallback text is shown immediately
});
export default function Home() {
return (
<main className={roboto.className}>
<h1>Welcome to My Site</h1>
<p>This text is in Roboto font without causing layout shifts.</p>
</main>
);
}
This will preload the font and use swap behavior, minimizing any shift when the font loads. Do: Use display: swap
for custom fonts (Next’s font loader does this by default) and preload your most important fonts. Don’t: Use fonts that load late and reflow text; avoid font-display: optional
(which can cause flash of unstyled text) unless you know what you’re doing.
Avoid Unexpected UI Inserts: Layout shifts often happen when new elements are added to the DOM above existing ones. For instance, avoid inserting a promo bar at the top after content has loaded, or if you must, ensure it doesn’t push the rest of the page (overlay it, or reserve space from the start). Similarly, be cautious with client-side rendering of content that wasn’t accounted for in initial HTML (with Next.js, if using SSR/SSG this is less an issue, but if some component appears conditionally on the client only, be mindful of its impact on layout).
Animation and Transitions: If you do animations, prefer CSS transform/transitions that do not trigger reflow (e.g., animate opacity
or transform
rather than height or width). For example, fading in an element (opacity) won’t shift the layout, whereas expanding its height might. Use the Chrome DevTools Layers panel to see if an animation triggers layout changes. Next.js doesn’t directly handle this, but it’s part of good frontend practice to keep CLS low.
CLS Optimization Do’s and Don’ts:
- Do: Use the Next.js Image component for all images, it prevents layout shifts by setting sizes and also lazy-loads below-the-fold images. Ensure every image, video, or embed has an explicit size or aspect ratio box reserved.
- Do: Use next/font or font-display swap for custom fonts. And preload critical fonts so text doesn’t jump when the font loads.
- Do: Reserve space for components that load late (ads, banners, etc.). Design your layout with placeholders for any content that might appear asynchronously.
- Don’t: Insert content above existing content without adjusting the layout from the start. For example, don’t suddenly drop a notification bar at the top that pushes everything down.
- Don’t: Use animations that alter layout properties (position/size) in a jarring way, animate in a way that doesn’t affect surrounding elements (use transforms or absolutely positioned overlays for entering content).
Monitoring and Testing Your Core Web Vitals¶
Optimizing is an ongoing process, you need to measure your Core Web Vitals to know if your Next.js optimizations are effective. Here are ways to test and monitor both in development (lab) and with real users (field):
Lighthouse (in Chrome DevTools or CI): Lighthouse is a lab testing tool that simulates page load and provides metrics like LCP, CLS, and Total Blocking Time (as a proxy for FID). In Chrome, open DevTools and go to the Lighthouse tab to generate a report. Lighthouse will flag things like long tasks or layout shifts and suggest improvements. It’s great for catching issues during development. Keep in mind Lighthouse uses a simulated mobile device and slow network by default, which is useful to uncover performance bottlenecks.
Chrome DevTools Performance Panel: For a more granular look, use the Performance panel. Record a page load and interaction to see timeline details. It will show when LCP occurred and highlight layout shift events (in the Experience track, CLS flashes are highlighted). You can also simulate throttling to see how your Next.js app performs under slow conditions. DevTools now even shows an overlay of Live Core Web Vitals (enable the "Web Vitals" overlay in Rendering tools) to watch LCP, FID, CLS in real time as you interact. This is useful for debugging what element contributed to LCP or which shifts contributed to CLS.
GTmetrix and WebPageTest: These are online tools that run your site in controlled lab environments (real browsers in specific locations) and give detailed reports. They both provide metrics including LCP and CLS (FID is not directly measured in lab). WebPageTest is very powerful, you can test with different connection speeds, see filmstrip views of your site loading, and even test multiple steps (like interaction). GTmetrix similarly gives waterfall charts and vital scores. Use these to test how your Next.js site behaves on various devices and networks.
PageSpeed Insights (PSI): This tool (pagespeed.web.dev) combines Lighthouse lab analysis and real-world field data (CrUX) if available. The top of a PSI report will show Field Data for your URL or origin, the 75th percentile LCP, FID (or INP, its upcoming replacement), and CLS from actual Chrome users. This is incredibly useful to track how you’re doing for real users over time. If your Next.js site has sufficient traffic, check PSI or Google Search Console’s Core Web Vitals report for field metrics. Aim to see “Good” (green) for all three metrics.
CrUX (Chrome User Experience Report): CrUX is a public dataset of real user experience metrics. You can use the CrUX Dashboard or BigQuery to monitor your site’s FID, LCP, CLS over time from real users. This is what Google uses for ranking, so field data is the gold standard. If your site is new or doesn’t have enough traffic, you might not have CrUX data, in that case rely on your own Real User Monitoring.
Real User Monitoring (RUM): Next.js has a built-in mechanism to report web vitals from real users in your app. By exporting a reportWebVitals
function in your _app.js
, Next.js will call it on the client with metrics (including LCP, FID, CLS, as well as other vitals like TTFB). You can send these to an analytics service or log them. For example:
// pages/_app.js
export function reportWebVitals(metric) {
console.log(metric.name, metric.value);
// You could also send this to an analytics endpoint for aggregation
}
Tip: Use a combination of lab and field tools. During development and CI, use Lighthouse or WebPageTest to catch issues early. After deployment, monitor field data via PageSpeed Insights or Search Console to ensure users are seeing the benefit.
Finally, keep in mind that optimizing Core Web Vitals is an iterative process, as you add features or content, keep testing and fine-tuning.
Conclusion¶
Core Web Vitals (LCP, FID, CLS) boil down to speed, responsiveness, and stability, qualities every user-friendly site should have. Next.js gives developers a head start with performance-minded defaults (like automatic code-splitting, image optimization, and caching), but it’s up to you to leverage those features effectively. By applying the strategies outlined, optimizing images, splitting code, deferring third-party scripts, using static rendering, and ensuring a stable layout, you can build Next.js applications that load fast, delight users, and rank higher on search engines.
Remember, performance is as important as any feature. A snappy, smooth Next.js app not only pleases Google’s algorithms but more importantly pleases your users, leading to better engagement and success for your site. Keep measuring your Web Vitals and iterating. With Next.js and the right practices, achieving excellent Core Web Vitals is an attainable goal.
FAQs
What are Core Web Vitals and why are they important for my Next.js site?
Core Web Vitals (LCP, FID, CLS) are performance metrics that measure loading, interactivity, and visual stability. Google uses them for SEO ranking and user experience evaluation.
How does Next.js help improve Core Web Vitals out of the box?
Next.js provides automatic image optimization, code splitting, static site generation, and script loading strategies, all of which improve LCP, FID, and CLS scores.
What are the most effective ways to improve LCP in Next.js?
Use the next/image
component for hero images, prioritize critical resources, serve static content via CDN, and minimize render-blocking scripts.
How can I test my Next.js site’s Core Web Vitals during development?
Use Lighthouse in Chrome DevTools, PageSpeed Insights, GTmetrix, WebPageTest, and Next.js’s reportWebVitals
API for both lab and real user data.
What are common mistakes that hurt Core Web Vitals in Next.js apps?
Using unoptimized images, loading too much JavaScript up front, not reserving space for dynamic content, and poorly configured third-party scripts are typical issues.