Fixing Page Speed in Next.js: Server Response and Load Time
A Next.js site can serve pages straight from a CDN in a few milliseconds, or rebuild every page on a server for every visitor while waiting on a database in another country. Which one you get depends on decisions that are easy to miss in the code.
This guide is about the server side of Next.js speed: how quickly the HTML arrives and how quickly the rest of the files follow. It shows how to find which routes are rendered on each request, why, and how to make them cached, parallel and close to their data.
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:
- Data requests one after another. Several
awaitcalls in a row, each waiting for the previous one, add their times together. - Distance. A serverless function in one region calling a database in another pays the round trip on every query.
- Middleware on every request. Code that runs before each request, including requests for images and scripts, adds time even to cached pages.
- Missing or wrong cache headers. A self-hosted app behind a proxy or CDN that sends
no-storeon everything will never be served from the edge. - Cold starts. On serverless hosting, a function that has not run recently needs time to start, which shows up as occasional slow responses.
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:
| Header | Value | Meaning |
|---|---|---|
x-nextjs-cache | HIT | Served from the Next.js cache for a static or revalidated route |
x-nextjs-cache | STALE | Served a cached copy while a fresh one is built in the background; still fast |
x-nextjs-cache | MISS every time | The cache is not being reused, often because each instance has its own cache |
x-nextjs-cache | absent | Usually a dynamic route rendered on each request |
cache-control | private, no-cache, no-store | Next.js has treated the route as dynamic; no CDN will cache it |
x-vercel-cache | HIT or MISS | On 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.
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.
- Next.js 14. In the App Router,
fetchrequests were cached by default unless opted out, and GET route handlers were cached by default. Many sites that upgraded found pages became slower because they relied on this. - Next.js 15.
fetchrequests and GET route handlers are no longer cached by default, and request APIs such ascookies(),headers()andparamsbecame asynchronous. Pages still prerender where they use no dynamic APIs, but data they fetch at request time is fetched fresh unless you say otherwise. - Next.js 16. Introduces an opt-in caching model built around the
use cachedirective and cache lifetime profiles, and renames middleware to proxy. If you enable the new model, the older route segment options such asrevalidateare replaced by it, so follow the upgrade guide rather than mixing the two.
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.
Keeping a Next.js site fast
- Check the route table in review. Save the
next buildroute table in your pull request or deployment log, and flag any route that changes from static to rendered on demand. - Set lifetimes deliberately. Give every data source an explicit cache lifetime and a tag, rather than relying on defaults that change between versions.
- Test first byte after deployment. Run the curl loop against production after each release and compare with the previous result.
- Read the upgrade guide for caching changes. Before each major upgrade, read the caching section of the release notes and re-test the routes that matter most.
- Keep the Page Speed Analyzer running on key routes. A regular check on one URL per route type catches a slow template before it affects every page built on it.
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.
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 โ