Routes in a Next.js application are rendered on the server by default. While this has its advantages, one notable downside is speed. When visiting a new route, clients must wait for the server to respond before it can display new content.
Prefetching is a Next.js feature that is designed to reduce that wait. A solid understanding of how prefetching works will help keep your application fast.
See also: Next.js Docs
Navigating to a route without prefetching sends a request to the server for a RSC payload. The payload is then used by the client to render the route's UI.
To request a RSC payload, clients send a GET request to the name of the route with RSC: 1
in the header. We'll refer to this type of request as a Navigation RSC Request.
GET /<route> HTTP/1.1
Host: <Next.js server>
RSC: 1
What the server does after receiving the request depends on if the route is static or dynamic.
In a static route:
In a dynamic route:
In both cases, the client must wait for the server to respond with the RSC payload before it can display new content. Prefetching minimizes the wait by requesting the payload before a user actually navigates to a route.
We'll look at how this works for the simpler case first, static routes.
Prefetching for static routes begins at build time, before any requests are made. Let's say we have this page.tsx
component that serves the /about
route:
// app/about/page.tsx
export default function About() {
return (
<div>
<h1>About Me</h1>
<p>Hi, my name is Jimmy.</p>
<ul>
<li>I like basketball and designing cool visuals.</li>
</ul>
</div>
);
}
As part of the build process, Next.js determines /about
is a static route.
$ next build
... # truncated
Route (app) Size First Load JS
┌ ○ / 162 B 103 kB
├ ○ /_not-found 990 B 101 kB
├ ○ /about 131 B 99.7 kB
└ ƒ /profile 131 B 99.7 kB
...
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
It then prerenders the route's response in two formats: HTML and RSC. After running next build
, you can see these responses saved as files at .next/server/app
:
.next/server/app/
... # truncated
├── about/
├── about.html # HTML response
├── about.meta
└── about.rsc # RSC response
The .html
file contains a non-interactive version of the page and is only sent during an initial request to /about
. The .rsc
file contains the aforementioned React Server Component payload. This payload is used whenever the user navigates to /about
from somewhere inside the app, and plays a key role in prefetching.
With prefetching, whenever a link to /about
appears in the user's viewport, Next.js automatically initiates a background request for the route's RSC payload.
The request looks like the Navigation RSC Request, except it includes a special Next-Router-Prefetch: 1
header. We'll refer to requests with this prefetch header as a Prefetch RSC Request.
GET /about HTTP/1.1
Host: <Next.js server>
RSC: 1
Next-Router-Prefetch: 1
The server responds with the payload that was prerendered and cached during build. This RSC payload contains everything the client needs to render the /about
page. Note how the body of the /about
component is directly encoded in the payload:
$ curl 'http://localhost:3000/about' -H 'RSC: 1' -H 'Next-Router-Prefetch: 1'
... # truncated
["$","div",null,{
"children":[
["$","h1",null,{"children":"About Me"}],
["$","p",null,{"children":"Hi, my name is Jimmy."}],
["$","ul",null,{"children":
["$","li",null,{"children":"I like basketball and designing cool visuals."}]
}]
]
}]
...
Now, when the user clicks the link to /about
, all the new content is already loaded on the client and the route can be rendered immediately:
Let's say we have this page.tsx
component that serves a dynamic route /profile
:
// app/profile/page.tsx
import { headers } from 'next/headers';
export default async function Profile() {
const headersList = await headers();
const userAgent = headersList.get('user-agent');
return (
<div>
<h1>Profile</h1>
<p>Your browser: {userAgent}</p>
</div>
);
}
Why is this route dynamic? This component uses the headers()
function, which requires server-side execution to read request headers. Since request headers vary per request, Next.js cannot prerender this route at build time.
As mentioned before, responses for dynamic routes are not prerendered during the build. This makes prefetching for dynamic routes a bit more involved.
Just like for static routes, whenever a link to /profile
appears in the user's viewport, Next.js automatically initiates a Prefetch RSC request.
GET /profile HTTP/1.1
Host: <Next.js server>
RSC: 1
Next-Router-Prefetch: 1
But since this is a dynamic route, the server has no prerendered payload it can return. Instead of running code to generate the payload, the server opts to return what is essentially an empty response. In other words, prefetching is skipped by default for dynamic routes .
$ curl 'http://localhost:3000/profile' -H 'RSC: 1' -H 'Next-Router-Prefetch: 1'
0:{"b":"woJZgBelu67N0IfkMHHX9",
"f":[[["",{"children":["profile",{"children":["__PAGE__",{}]}]},
"$undefined","$undefined",true],
null,[null,null],true]],
"S":false}
Now, when the user clicks the link to /profile
, the client the initiates a Navigation RSC Request to get the actual payload for the route. But because nothing was prefetched, the client has to wait for this response before it can show any new content, which can make it feel like the application is unresponsive.
The client waits for the server to respond with the payload before showing any new content. I added an artificial delay to the server response to make it more noticeable.
To get the benefits of prefetching for dynamic routes, include a loading.tsx
component in the same directory as the page.tsx
component.
// app/profile/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse">
<h1>Loading...</h1>
<p>Please wait while we load the profile...</p>
</div>
);
}
When loading.tsx
is defined, the server responds to the prefetch RSC request with the loading component!
$ curl 'http://localhost:3000/profile' -H 'RSC: 1' -H 'Next-Router-Prefetch: 1'
... # truncated
"loading": [["$","div","l",{
"className":"animate-pulse",
"children":[
["$","h1",null,{"children":"Loading..."}],
["$","p",null,{"children":"Please wait while we load the profile..."}]
]
}],[],[],false]},null,false]
...
Now, when the user clicks the link to /profile
, the loading UI is shown immediately, without having to wait for a server response. The client will still then initiate the navigation RSC request to get the actual payload for the route, which the client uses to replace the loading UI when it returns.
The loading UI is shown immediately, without having to wait for a server response.
The key is that loading.tsx
gives the client something meaningful it can prefetch. This results in a better user experience as the user sees the loading UI immediately while waiting, making the app feel much more responsive.
This behavior only applies to Next.js <Link>
components, not regular HTML anchor tags. See Next.js Link documentation for details.