#Next.js Prefetching Deep Dive

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

#Rendering a Route (no prefetching)

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:

Your browser does not support SVG

In a dynamic route:

Your browser does not support SVG

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.

#Static Routes

#Next Build

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.

#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.

Demo showing Next.js automatic prefetching when a link enters the viewport

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:

Your browser does not support SVG

#Dynamic Routes

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.

#Default Case

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.

Demo showing Next.js automatic prefetching when a link enters the viewport

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.

Your browser does not support SVG

#Loading.tsx

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.

Demo showing Next.js automatic prefetching when a link enters the viewport

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.

Your browser does not support SVG


[1]

This behavior only applies to Next.js <Link> components, not regular HTML anchor tags. See Next.js Link documentation for details.

[2]

This "avoids unnecessary work on the server for routes the user may never visit."