WritingHow to Optimize Next.js Performance: Core Web Vitals and Beyond — Clixo
6 min readnext.js, performance, core-web-vitals, optimization, app-router

How to Optimize Next.js Performance: Core Web Vitals and Beyond

A practical guide to optimizing Next.js application performance — covering bundle size, image loading, font rendering, caching, and Core Web Vitals improvements.

A Next.js application that scores well in development often surprises teams when it hits real users on slow connections and mid-range devices. Core Web Vitals regressions, bloated JavaScript bundles, and layout shifts are the most common performance issues — and most of them have straightforward fixes that do not require a full architectural overhaul.

This guide covers the highest-impact optimizations, in roughly the order you should address them.

Start With Measurement

Before optimizing anything, establish a baseline. Run your production build (not next dev) and measure it. Development mode disables many optimizations and gives misleading results.

Tools to use:

  • Lighthouse in Chrome DevTools, run in a private window with no extensions
  • Vercel Speed Insights if you deploy on Vercel — it collects real-user metrics
  • @next/bundle-analyzer to visualize what is in your JavaScript bundles

Look for the three Core Web Vitals: Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS), and Interaction to Next Paint (INP). These are the metrics search engines and users both care about.

Reduce JavaScript Bundle Size

Large client-side bundles are the most common cause of slow LCP and high INP. The App Router helps significantly because server components do not ship any JavaScript to the client — but client components and their imports do.

Audit your client components. Run the bundle analyzer and look for large packages being pulled into the client bundle. Common culprits: date formatting libraries imported into client components (move the formatting to the server), large icon libraries imported entirely (import individual icons instead), and charting libraries loaded on pages that only show them conditionally.

Use dynamic imports for heavy client components. If a component is only needed after user interaction — a rich text editor, a complex data grid — import it dynamically so it does not block initial load.

import dynamic from 'next/dynamic'
 
const RichEditor = dynamic(() => import('./rich-editor'), {
  loading: () => <p>Loading editor...</p>,
  ssr: false,
})

Move logic to the server. Any data transformation, filtering, or formatting that does not need to happen in the browser should happen in a server component. Library code that only runs on the server is never shipped to the client.

Optimize Images with next/image

The Image component from next/image is one of the highest-leverage performance improvements available with almost no effort. It automatically:

  • Serves modern formats (WebP, AVIF) to browsers that support them
  • Generates responsive sizes and serves the right one for the viewport
  • Lazy loads images below the fold
  • Reserves space for the image so it does not cause layout shift

Use it for every image in your application. For above-the-fold images (the hero, the product photo on a product detail page), add the priority prop to pre-load the image and improve LCP.

import Image from 'next/image'
 
<Image
  src="/hero.jpg"
  alt="Product hero"
  width={1200}
  height={600}
  priority
/>

If you forget priority on the LCP image, it will be lazy-loaded and your LCP score will suffer.

Load Fonts Without Layout Shift

External font requests delay rendering and cause layout shift (CLS) as the font swaps in. next/font eliminates both problems by downloading fonts at build time, self-hosting them, and injecting CSS variables with no external network request at runtime.

import { Inter, Playfair_Display } from 'next/font/google'
 
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
const playfair = Playfair_Display({ subsets: ['latin'], variable: '--font-playfair' })

Apply the variables to the root html element in your root layout. No @font-face declarations, no preload links, no layout shift.

Use Streaming and Suspense for Slow Data

If a page has one slow data dependency, it blocks the entire page from rendering. Wrapping slow components in Suspense lets the fast parts stream to the user immediately.

import { Suspense } from 'react'
import { ProductReviews } from './product-reviews'
import { ReviewsSkeleton } from './reviews-skeleton'
 
export default function ProductPage() {
  return (
    <div>
      <ProductInfo />   {/* fast, renders immediately */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews />  {/* slow, streams in when ready */}
      </Suspense>
    </div>
  )
}

The user sees the product information immediately. The reviews load in when the database query resolves. LCP improves because the main content is not blocked by a secondary query.

Configure Caching Aggressively

Every page or data fetch that does not need to be dynamically rendered should be cached. Cached responses are served from a CDN edge, with Time to First Byte under 100ms. Uncached responses hit your origin server on every request.

For frequently-read, infrequently-changed data (product catalog, blog posts, documentation), use ISR with an appropriate revalidate window. For rarely-changed data (pricing tiers, navigation structure), use a long revalidation window or rebuild on demand when content changes.

Eliminate Render-Blocking Resources

Check your production build for anything that blocks rendering:

  • Scripts that load synchronously in the head. Move third-party scripts to next/script with strategy="lazyOnload" or strategy="afterInteractive".
  • Large CSS imports. Tailwind's JIT mode ships only the CSS you use. If you are importing large third-party CSS files, check whether you need all of it.
  • Fonts loaded from external URLs. Replace with next/font.

Optimize the Root Layout

The root layout.tsx wraps every page in your application. Any heavy computation or slow import in the root layout affects every route. Keep it lean: metadata, font variables, global providers, and nothing else.

Measure After Each Change

Performance optimization is iterative. Run Lighthouse before and after each change to confirm the improvement is real and no regression appeared elsewhere. A change that improves LCP sometimes increases CLS if it affects how content loads above the fold.

The goal is not a single Lighthouse run that looks good. It is a production application that feels fast to real users on real devices and connections.


Next.js provides the tools for excellent performance. Using them well requires understanding which problem each tool solves and measuring consistently.

If you're building a Next.js application and need a team that ships fast by default, Clixo works with product teams to design and deliver performant applications from the first line of code.