Skip to content
Wadhah Belhassen
← All articlesWeb Performance

Next.js Performance Best Practices: How to Ship Fast Apps in 2026

Next.js performance best practices — App Router, RSC, streaming, image optimization, fonts, third-party scripts. The patterns that ship fast production apps.

Wadhah Belhassen2026-11-0613 min read
Next.js Performance Best Practices: How to Ship Fast Apps in 2026

Next.js performance best practices have evolved significantly with the App Router, React Server Components, and streaming. Many performance "tips" you find online still reference the Pages Router and miss the patterns that actually matter in 2026. The result is teams shipping Next.js apps that should be fast by default — and are not.

This guide covers the Next.js performance practices we apply on every production build. App Router patterns, Server Components, streaming, image optimization, fonts, third-party scripts, and the architectural decisions that determine whether your app feels instant or sluggish.

The work is technical. Done right, a properly-built Next.js app in 2026 ships with green Core Web Vitals out of the box and stays green through feature work.

Why Next.js performance matters more than framework choice

The Next.js team has done excellent work making the default experience fast. The catch is that the defaults only stay fast if developers understand the framework's performance model.

Most performance issues we audit on Next.js apps come from one of three sources:

  • Using client components when server components would work
  • Loading third-party scripts in the wrong way
  • Misunderstanding the caching layers

Each is a developer choice, not a framework limitation. Fix the choices and Next.js delivers.

We covered the broader Core Web Vitals foundation in our Core Web Vitals deep dive guide. This guide goes Next.js-specific.

Section 1 — Use Server Components by default

The single biggest Next.js performance win in 2026 is using React Server Components (RSC) for everything that does not require client-side interactivity.

What Server Components actually do

Server Components render on the server and ship as HTML plus a serialised React tree. They do not ship their JavaScript to the client.

Result: smaller JavaScript bundles, faster page loads, less work for the browser.

Default to server, opt into client

In App Router, every component is a Server Component by default. The "use client" directive opts a component (and its descendants) into Client Component behaviour.

The discipline:

  • Never add "use client" to a component unless it genuinely needs useState, useEffect, event handlers, or browser APIs
  • When you do add "use client", push it as deep into the tree as possible
  • Pages, layouts, and most UI structure should be Server Components

Common over-use of Client Components

The most common Next.js performance issue we see: an entire page wrapped in "use client" because one button inside needs an onClick.

Fix: extract the interactive button into its own small Client Component. Keep the rest of the page server-rendered.

When to use Client Components

  • Forms with controlled inputs
  • Interactive elements (dropdowns, modals, toggles)
  • Browser APIs (geolocation, localStorage, navigator)
  • Real-time updates (WebSockets, polling)
  • Third-party libraries that require client-side execution (some charting libraries, animation libraries)

Everything else should be a Server Component.

Section 2 — Streaming and Suspense

Streaming lets Next.js send parts of the page as they become ready, instead of waiting for all data before rendering anything.

Why streaming matters for LCP

For pages with multiple data dependencies, traditional server-rendering waits for all data before rendering. LCP suffers.

With streaming, the page shell renders immediately. Slow data sections render later. The fast parts of the page (hero, headlines, navigation) hit LCP fast, slow parts (recommendations, reviews, analytics widgets) hydrate behind Suspense boundaries.

Implementing streaming with Suspense

// app/products/[slug]/page.tsx
import { Suspense } from "react";

export default function ProductPage({ params }) {
  return (
    <main>
      <ProductHero slug={params.slug} />
      <Suspense fallback={<RelatedProductsSkeleton />}>
        <RelatedProducts slug={params.slug} />
      </Suspense>
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews slug={params.slug} />
      </Suspense>
    </main>
  );
}

The product hero blocks render until ready (it is the LCP). Related products and reviews stream in afterward.

loading.tsx for instant navigation feedback

Place a loading.tsx file in any route folder. Next.js shows it immediately on navigation, before the page's data loads.

This makes navigation feel instant even when actual data takes a few hundred ms to fetch.

Section 3 — Image optimization with next/image

The next/image component is one of the strongest reasons to use Next.js. Use it correctly and image performance is solved.

Always use next/image for content images

import Image from "next/image";

export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero"
      width={1200}
      height={630}
      priority
      sizes="(max-width: 768px) 100vw, 1200px"
    />
  );
}

What this does for you automatically:

  • Serves modern formats (WebP, AVIF) when the browser supports them
  • Generates responsive sizes via srcset
  • Lazy-loads images below the fold
  • Prevents layout shift through reserved dimensions

priority for the LCP image

The image that is the LCP should have priority. This sets fetchpriority="high" and loading="eager".

Only one image per page should have priority. Marking everything as priority defeats the purpose.

Configure remote patterns

For images from external sources:

// next.config.ts
export default {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "cdn.example.com" }
    ]
  }
};

Without remote patterns, external images are not optimised by Next.js.

Use sizes correctly

The sizes attribute tells the browser which image size to fetch for the current viewport. Wrong sizes value means the browser downloads a larger image than needed.

For a full-width hero: sizes="100vw". For a 2-column grid: sizes="(max-width: 768px) 100vw, 50vw".

We covered the full image optimization framework in our image optimization guide.

Section 4 — Fonts with next/font

The next/font system eliminates font-loading performance issues that plague non-framework sites.

Self-host Google Fonts

import { Inter } from "next/font/google";

const inter = Inter({
  subsets: ["latin"],
  display: "swap",
});

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

This downloads the font at build time, self-hosts it on your domain, and applies font-display: swap. No third-party requests to fonts.googleapis.com. No flash of invisible text.

Subset by language

const inter = Inter({
  subsets: ["latin", "latin-ext"],
  display: "swap",
});

Only loading the character ranges you actually use cuts font file size dramatically. A full Inter file is 200+ KB. Latin-only subset is around 30 KB.

Use variable fonts where possible

A variable font contains multiple weights and styles in a single file. Instead of loading 4 different files for Regular, Medium, Bold, and Italic, one file covers them all.

Next.js supports variable fonts natively. Use them where possible.

Section 5 — Third-party scripts with next/script

Third-party scripts (analytics, chat widgets, marketing tags) are the most common Next.js performance killer.

Use the Script component

import Script from "next/script";

export default function Layout({ children }) {
  return (
    <>
      {children}
      <Script
        src="https://www.googletagmanager.com/gtag/js?id=GA_ID"
        strategy="afterInteractive"
      />
    </>
  );
}

Strategies available:

  • beforeInteractive: loads before page becomes interactive. For critical scripts only (rare).
  • afterInteractive: loads after the page is interactive. Default for analytics.
  • lazyOnload: loads after page is idle. For chat widgets, video players, social embeds.
  • worker: loads in a web worker via Partytown. Experimental but powerful.

Default to afterInteractive for analytics

Google Analytics, Tag Manager, Plausible, Fathom — all should be afterInteractive. They do not need to block first paint.

Use lazyOnload for non-critical scripts

Intercom, Drift, Crisp chat — chat widgets do not need to load before page is idle. lazyOnload defers them.

Audit which scripts are actually needed

Most apps accumulate scripts over time. Audit annually. Each script costs INP and LCP.

We covered third-party script management in depth in our third-party script management guide.

Section 6 — Data fetching patterns

Data fetching in App Router uses fetch() with built-in caching.

Use server-side fetch when possible

// app/products/page.tsx
export default async function ProductsPage() {
  const products = await fetch("https://api.example.com/products").then(r => r.json());
  return <ProductList products={products} />;
}

Server-side fetch is faster than client-side fetch because:

  • The request runs from the server (often closer to the database)
  • No JavaScript bundle waterfall
  • Result is included in the initial HTML

Cache aggressively

Default fetch() caches at build time (force-cache). Add cache hints for dynamic data:

const products = await fetch(url, { next: { revalidate: 3600 } }).then(r => r.json());

This caches for 1 hour, then revalidates. Most pages can tolerate this — true real-time data is rarer than people think.

Parallel data fetching

const [products, reviews, categories] = await Promise.all([
  fetch("/api/products"),
  fetch("/api/reviews"),
  fetch("/api/categories"),
]);

Independent fetches should run in parallel. Sequential awaits cause waterfalls.

Use unstable_cache for expensive computations

import { unstable_cache } from "next/cache";

const getProducts = unstable_cache(
  async () => /* expensive query */,
  ["products"],
  { revalidate: 3600 }
);

Cache expensive computations server-side. Especially useful for database queries that do not change frequently.

Section 7 — Rendering strategies

Next.js App Router defaults to static rendering. Understand the options.

Static (SSG)

Generated at build time. Cached and served as static files. Fastest possible delivery.

Best for: marketing pages, blogs, documentation, anything that does not change per-user.

Dynamic (SSR)

Generated on every request. Personalised, real-time, but slower.

Best for: authenticated pages, personalised dashboards, real-time data.

Incremental Static Regeneration (ISR)

Generated at build time, then revalidated on a schedule. Combines static speed with content freshness.

Best for: e-commerce product pages, news sites, anything that changes infrequently.

We covered the SSR vs SSG vs ISR decision in detail in our SSR vs SSG vs ISR guide.

Section 8 — Bundle size management

Even with RSC reducing client bundles, bundle size still matters for the JavaScript that ships.

Run bundle analysis regularly

ANALYZE=true npm run build

With @next/bundle-analyzer configured, this produces a visualisation of bundle composition. Audit it monthly.

Code-split heavy components with dynamic()

import dynamic from "next/dynamic";

const HeavyChart = dynamic(() => import("./HeavyChart"), {
  loading: () => <div>Loading...</div>,
  ssr: false,
});

Use for:

  • Charts and data visualisation
  • Rich text editors
  • Map components
  • Anything 100+ KB that is not on the critical path

Audit third-party libraries

Common offenders:

  • Moment.js (500+ KB) → swap for date-fns or Day.js
  • Lodash whole-package import → use specific function imports
  • jQuery in a React app → almost always unnecessary
  • Full UI libraries when only 2 components are used → tree-shake or replace

Section 9 — Turbopack and build performance

Next.js 16 uses Turbopack by default for development and production builds.

Use the latest Next.js version

Each Next.js version brings performance improvements. Staying current matters.

For production, run the latest stable. For experimentation, the latest canary often has performance wins worth testing.

Build performance vs runtime performance

Slow builds slow down development, not user experience. But fast builds enable more frequent deploys, which enables faster iteration on user-facing performance.

Output Standalone for smaller Docker images

// next.config.ts
export default {
  output: "standalone"
};

This produces a minimal standalone bundle for Docker deploys. Faster cold starts, smaller container images.

A real example — Next.js e-commerce site

We migrated a Next.js 14 Pages Router e-commerce site to Next.js 16 App Router. The migration delivered:

  • LCP from 3.2s to 1.4s (faster image delivery, fewer Client Components)
  • INP from 480 ms to 140 ms (less client JavaScript)
  • CLS from 0.18 to 0.03 (better image dimension handling)
  • 60 percent reduction in client JavaScript bundle size

The full conversion impact: 41 percent revenue lift in the following 90 days. The full story is in our Marseille cosmetics case study.

A 30-day Next.js performance optimization plan

If you have a Next.js app that is underperforming, follow this sequence.

Days 1 to 5 — Baseline. Run Lighthouse on top 5 pages. Run bundle analyzer. Identify the worst metric and biggest bundle issues.

Days 6 to 12 — RSC audit. Find Client Components that should be Server Components. Push "use client" deeper into the tree. Reduce client bundles.

Days 13 to 17 — Images and fonts. Verify all images use next/image correctly. Add priority to LCP. Move to next/font.

Days 18 to 23 — Third-party scripts. Audit every script. Use next/script with appropriate strategies. Defer non-critical scripts.

Days 24 to 28 — Data fetching. Move client-side fetches to server. Add cache hints. Parallelise independent fetches.

Days 29 to 30 — Measure. Re-run Lighthouse. Compare to baseline. Plan ongoing performance budget.

Most Next.js apps move from poor to good Core Web Vitals in this window.

Common Next.js performance mistakes

These are the patterns we see most often.

Wrapping entire pages in "use client". Defeats Server Components. Push it deeper.

Using <img> instead of next/image. Misses automatic optimization.

Loading Google Fonts via stylesheet. Use next/font instead.

Synchronous third-party scripts in <head>. Block render. Use next/script with appropriate strategy.

Skipping sizes on next/image. Browser downloads larger image than needed.

Sequential awaits for independent data. Use Promise.all for parallel fetching.

Not running bundle analyzer. You cannot reduce what you cannot see.

Trusting Lighthouse score over CrUX field data. Lighthouse is lab data. Optimise for real users.

Frequently asked questions

Is App Router really faster than Pages Router?

In most cases yes, with proper use of Server Components. The biggest wins come from reducing client-side JavaScript. Sites with heavy interactivity see smaller wins.

Should I migrate from Pages Router to App Router?

For new projects, App Router is the only choice. For existing Pages Router apps, migrate if you can dedicate engineering time. The performance and DX benefits compound over the next several years.

Do I need a CDN if I'm hosted on Vercel?

Vercel's edge network is a CDN. For static assets and ISR pages, it is already optimised. For custom domains, ensure your domain is pointing through Vercel correctly.

How important is the priority attribute on next/image?

Critical for the LCP image. Without it, the LCP image waits behind other resources. With it, LCP often improves by 200 to 800 ms.

What's the difference between next/script strategies?

beforeInteractive runs before page is interactive (rarely needed). afterInteractive runs after (default for analytics). lazyOnload runs after page is idle (chat widgets, embeds). worker runs in a web worker via Partytown.

Can I run Next.js on cheap hosting?

Technically yes, but performance suffers. Modern managed hosts (Vercel, Cloudflare Workers, modern Node hosts) deliver dramatically better TTFB than traditional shared hosting.

Get a Next.js performance audit

We audit Next.js apps free of charge. Within 48 hours we deliver a per-page breakdown of performance issues with specific fixes and expected impact.

Book a free 30-minute audit. We screen-share, walk through your Lighthouse and bundle data, and you leave with a clear action plan.

Or explore our Web Development service for the full system we run on performance-focused client accounts.

Want these strategies applied to your business?

30 minutes of free audit with concrete recommendations tailored to your business.