/ โ† Page Speed Fixes / Next.js
In this guide
  1. Why Next.js pages get slow at the server
  2. Measuring server response per route
  3. The fix, step by step
  4. Code patterns that keep routes fast
  5. Caching changes between Next.js versions
  6. Self-hosting and platform notes
  7. Keeping a Next.js site fast
  8. Questions and answers

Why Next.js pages get slow at the server

In Next.js, every route ends up as one of three things. It can be built once at build time and served as a file. It can be built, cached, and rebuilt in the background after a set time. Or it can be rendered on the server for each request. The first two are about as fast as a web page can be served. The third is only as fast as the slowest thing the server waits for while rendering.

The trouble is that a route can move from the first group to the third without anyone deciding it should. Reading a cookie in a shared layout, reading the query string, or a data request marked as uncached is enough. Once that happens, every visit waits for the server to render the page, and if the render waits for an API or database, the visitor waits too.

The other common causes sit around the rendering itself:

These are mostly problems of Time to First Byte, which is not one of the Core Web Vitals but delays everything that follows it, including the main content. For the front-end side of the framework, such as image and font components and hydration cost, see Core Web Vitals in Next.js and Nuxt.

Measuring server response per route

Start with the Page Speed Analyzer on one URL for each route type: the home page, a content page, an article, a product or listing page, and any search or account pages. Note the server response time for each. Then check each route from the command line so you can see the cache headers alongside the timing:

for u in / /about /blog/spring-update /products/oak-table; do
  curl -s -o /dev/null -D /tmp/h.txt \
    -w "$u  first byte: %{time_starttransfer}s  total: %{time_total}s\n" \
    "https://www.example.co.uk$u"
  grep -iE "^(cache-control|x-nextjs-cache|x-vercel-cache|age):" /tmp/h.txt
done

Run it two or three times. The first request may miss the cache; later ones should hit it. What the headers tell you:

HeaderValueMeaning
x-nextjs-cacheHITServed from the Next.js cache for a static or revalidated route
x-nextjs-cacheSTALEServed a cached copy while a fresh one is built in the background; still fast
x-nextjs-cacheMISS every timeThe cache is not being reused, often because each instance has its own cache
x-nextjs-cacheabsentUsually a dynamic route rendered on each request
cache-controlprivate, no-cache, no-storeNext.js has treated the route as dynamic; no CDN will cache it
x-vercel-cacheHIT or MISSOn Vercel, whether the edge cache answered

A route that should be the same for every visitor but shows no cache hit and a slow first byte is your first target. A route that must be dynamic but is slow needs its data work examined instead.

๐Ÿ’ก Test from roughly where your visitors are. A UK site measured from a laptop in London against a server in the United States will show a very different first byte from one measured on the same continent as the server.

The fix, step by step

Step 1

Time the server response for each route

Pick one URL for every route type and run each through the Page Speed Analyzer, noting the server response time. Then request each URL with curl two or three times, recording the time to first byte and the cache-control, x-nextjs-cache and, on Vercel, x-vercel-cache headers. List which routes are served from cache and which are rendered on each request. Routes that should be identical for all visitors but are never cached go to the top of the list.

Step 2

Read the build output route by route

Run next build and read the route table it prints at the end. Each route is marked with a symbol showing whether it was prerendered as static content, generated from parameters, or will be rendered on demand for each request. Compare this with what you expected. A marketing page, article or product page marked as rendered on demand is usually a mistake worth fixing. Save the output so you can compare it after every dependency upgrade or large change.

Step 3

Remove accidental dynamic rendering

For each route that became dynamic unexpectedly, search the page, its layouts and the components they use for cookies(), headers(), the searchParams prop, connection(), cache: 'no-store' and export const dynamic. Move per-visitor logic, such as reading a login cookie for the header, into a small client component or a separate dynamic section. Handle filters in the query string on the client, or give each filter its own static path where it matters for search.

Step 4

Cache data and set revalidation times

In Next.js 15 and later, fetch requests are not cached by default, so data a page needs may be fetched on every render. Give each data source an explicit lifetime: next: { revalidate: 3600 } on a fetch, export const revalidate on the route, or the use cache directive where your version supports it. For database calls, wrap the query in the framework's cache function. Use tag-based revalidation from your CMS webhook so edits appear quickly without shortening every lifetime.

Step 5

Run independent server requests in parallel

Look for server components and route handlers that await several data requests one after another, such as the product, then its reviews, then related items. Where a request does not need the result of the previous one, start them together with Promise.all so the total wait is the slowest request rather than the sum. Deduplicate repeated calls for the same data within a render using React's cache function, so a layout and a page asking for the same record trigger one query.

Step 6

Keep middleware narrow and redirects in config

Middleware, renamed proxy in Next.js 16, runs before matching requests, including cached pages. Add a matcher so it skips _next/static, _next/image, images and other files that never need it. Avoid database or API calls inside it; check a signed cookie instead. Move fixed redirects into the redirects function in the Next.js config file, which is applied without running your own code, and link internally to final addresses so visitors skip the redirect.

Step 7

Check cache headers on HTML and static files

Request a static route and a file under /_next/static/ and read the cache-control header on each. Hashed build files should carry a long, immutable lifetime, which Next.js sets itself. Static and revalidated HTML should carry a shared cache lifetime such as s-maxage with stale-while-revalidate. If you self-host behind nginx or a CDN, make sure neither strips these headers, adds no-store, or refuses to cache HTML at all.

Step 8

Run the server near its data behind a CDN

Check which region your server or serverless functions run in and where the database and main APIs live. Put them in the same region, ideally near most of your visitors. On Vercel set the function region in project settings; when self-hosting, choose a data centre close to both. Put a CDN in front so cached HTML and static files are served from the edge. Then repeat the curl and analyzer tests and compare first byte times.

Code patterns that keep routes fast

Before: a route that renders on every request

// app/(site)/layout.tsx - shared by every marketing page
import { cookies } from 'next/headers';

export default async function SiteLayout({ children }) {
  const session = (await cookies()).get('session');   // makes every page dynamic
  const nav = await fetch('https://cms.example.com/api/nav').then(r => r.json()); // uncached
  const footer = await fetch('https://cms.example.com/api/footer').then(r => r.json()); // waits for nav
  return (
    <>
      <Header nav={nav} signedIn={!!session} />
      {children}
      <Footer data={footer} />
    </>
  );
}

After: cached, parallel, with the per-visitor part split out

// app/(site)/layout.tsx
import AccountLink from './account-link'; // 'use client'; reads session in the browser

async function getNav() {
  return fetch('https://cms.example.com/api/nav', {
    next: { revalidate: 3600, tags: ['nav'] },
  }).then(r => r.json());
}
async function getFooter() {
  return fetch('https://cms.example.com/api/footer', {
    next: { revalidate: 86400, tags: ['footer'] },
  }).then(r => r.json());
}

export default async function SiteLayout({ children }) {
  const [nav, footer] = await Promise.all([getNav(), getFooter()]);
  return (
    <>
      <Header nav={nav}><AccountLink /></Header>
      {children}
      <Footer data={footer} />
    </>
  );
}

The layout no longer reads cookies on the server, so pages beneath it can be static. The two CMS requests are cached for an hour and a day, run at the same time, and can be refreshed early from a CMS webhook that calls revalidateTag. Check the exact arguments for your version, as the revalidation API has changed between releases.

Middleware that skips static files

// middleware.ts (proxy.ts in Next.js 16 and later)
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|images/|robots.txt|sitemap.xml).*)'],
};

Fixed redirects in config

// next.config.js
module.exports = {
  async redirects() {
    return [
      { source: '/old-services', destination: '/services', permanent: true },
      { source: '/blog/:slug/amp', destination: '/blog/:slug', permanent: true },
    ];
  },
};

Caching changes between Next.js versions

Caching behaviour is the part of Next.js that has changed most between major versions, which is why advice copied from older articles often does not work. Confirm the behaviour in the documentation for the version you run.

The Pages Router works differently. getStaticProps with a revalidate value gives incremental regeneration. getServerSideProps runs for every request and sends no shared cache header unless you set one, for example with res.setHeader('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300') when the page is the same for all visitors.

Self-hosting and platform notes

Vercel

Static and revalidated pages are cached at the edge automatically. The main settings to check are the function region, which should be near your database, and whether routes you expected to be cached show x-vercel-cache: HIT. Functions that run rarely can start slowly; keeping pages static avoids that for most visitors.

Node server behind nginx

Build with output: 'standalone' for a smaller deployment and run it with a process manager. Next.js compresses responses with gzip by default; if nginx or your CDN handles compression, set compress: false so the work is not done twice and Brotli can be used at the proxy. Let nginx serve /_next/static directly from disk with a long lifetime if you prefer, and make sure it passes the app's cache-control headers through for HTML.

More than one instance

Each self-hosted instance keeps its own cache of rendered and revalidated pages by default. Behind a load balancer this means different visitors may hit cold caches, and revalidation on one instance does not update the others. Configure a shared cache handler, for example backed by Redis, when you run several instances.

Images

The built-in image optimisation resizes images on the server the first time each size is requested, which can make first requests slow and uses processor time on your server. Make sure its cache persists between deployments, or use an external image service or CDN with the image component's loader option.

โš ๏ธ Do not cache pages at a CDN that show visitor-specific content, such as a basket count or a name, unless that part is loaded separately in the browser. Split per-visitor parts out before adding shared cache headers.

Keeping a Next.js site fast

Questions and answers

1. Why is my Next.js site slow even though it is server rendered?

Server rendering only helps if the server can answer quickly. If a route is rendered on every request and waits on an uncached API or a database in another region, the visitor waits for all of that before the first byte arrives. Check the build output to see which routes render on demand, look for cookies, headers or uncached fetches in shared layouts, cache data with explicit lifetimes, and run independent requests in parallel so the render finishes sooner.

2. How do I know if a Next.js route is static or dynamic?

Run next build and read the route table printed at the end. Each route has a symbol showing whether it was prerendered as static content, generated from parameters, or will be rendered on demand for each request. You can also check a deployed page with curl: static and revalidated routes usually carry an x-nextjs-cache header and a shared cache-control lifetime, while dynamic routes usually send private, no-cache, no-store and show no cache header.

3. Why did my pages get slower after upgrading to Next.js 15?

The most common reason is the change in caching defaults. In Next.js 14, fetch requests in the App Router were cached by default. From Next.js 15 they are not, so data that was previously reused is fetched again on every render. Add explicit revalidate values or cache tags to the fetches that feed your pages, or opt the route into caching, then compare first byte times before and after. Read the upgrade guide for other changes.

4. Does middleware slow down Next.js?

It can. Middleware runs before every request it matches, including requests for pages that are otherwise served from cache, and without a matcher that can include images and scripts too. Each run adds a little time, and much more if it calls a database or an external API. Limit it with a matcher that skips static files, keep it to quick checks such as reading a signed cookie, and move fixed redirects into the Next.js config instead.

5. Should I use the edge runtime to make pages faster?

Only for small, quick tasks. The edge runtime starts quickly and runs close to visitors, but it supports a limited set of Node.js APIs, and running a page at the edge does not help if that page then queries a database in one region, because every query still travels to that region. For most sites, caching pages so they do not need rendering at all, and running the server next to the database, gives larger gains.

6. Where should I host a Next.js site for UK visitors?

Somewhere that keeps the server, the database and the visitors close together. For a mainly UK audience, a London or nearby European region for both the application and the database avoids long round trips on every query. If you use Vercel, check the function region in project settings, because it may not default to one near you. A CDN in front then serves cached pages and static files from locations near each visitor.

7. Is Next.js TTFB a Core Web Vital?

No. Time to First Byte is not one of the Core Web Vitals, which are Largest Contentful Paint, Interaction to Next Paint and Cumulative Layout Shift. It is still worth fixing, because the browser cannot start loading anything else until the first byte of HTML arrives, so a slow server response delays the main content and therefore Largest Contentful Paint. Treat it as a diagnostic measure that explains slow page loads rather than as a target on its own.

Check how fast your Next.js routes respond

The Page Speed Analyzer measures your page with real-user and lab data and names the specific server responses, files and scripts causing each slow metric, so you can start with the fix that saves the most time.

Open Page Speed Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All Page Speed Fixes  ·  Fixing Slow Time to First Byte  ·  Core Web Vitals in Next.js and Nuxt  ·  Images in Next.js  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.