RSC Payload

May 31, 2024

React handles Server Components (RSCs) differently than standard Server Side Rendered pages. With standard SSR, React render a page's HTML on the server so it can be sent down the wire to a client as soon as possible, helping the user experience by showing meaningful content faster — and improving largest contentful paint, a core web vital metric.

That said, the initial HTML is basically just a visual snapshot of the way the page should look; it isn't interactive until React takes over and re-renders the page with all the necessary event listeners — and for React to do this, it needs code describing the entire DOM. This is a really important point: Even though the page gets rendered on the server, its JavaScript is needed for React to render the page again on the client.

You can see this using the pages router with nextjs:

// ssr-page.js in the pages router
export default function PagesRouterPage({ serverState }) {
  return <div data-id="ssr-pages-router">{serverState}</div>;
}

export async function getServerSideProps() {
  const serverState = "Hello from the server!";
  return { props: { serverState } };
}

Visit this page in your browser and open the network tab, then inspect ssr-page.js and search for ssr-pages-router. You'll see it's included in a function on a line beginning with something like eval(__webpack_require__.ts( ... \"data-id\": \"ssr-pages-router\",\n

This is not the case with server components. They are rendered on the server and sent to the client as HTML without any additional JavaScript added to the bundle. To see this, create a page in the nextjs app router like the following:

// app-router-page.js in the app router
export default async function AppRouterPage() {
  const data = await getData();
  return <div data-id="ssr-app-router">{data.props.serverState}</div>;
}

async function getData() {
  const serverState = "Hello from the server!";
  return { props: { serverState } };
}

Now inspect app-router-page.js and search for ssr-app-router. Wait a minute — there isn't a file called app-router-page! Search through whatever js files do appear in your dev tools, and you won't find any reference to ssr-app-router.

That's pretty neat: the JavaScript of server components does not run on the client, so the client doesn't need to download additional bundles; there's less JavaScript to parse and web vitals will likely improve.

Of course, React still needs to know about the entire DOM so it can mount and hydrate interactive components. Nextjs handles this by using a special React-formatted object called the RSC Payload. Inspect the app-router example above further and look at the html instead of the js files. You'll see a few <script> tags at the end of the page (or a lot more than a few, if you're inspecting a real app). This is the RSC payload and it's where you'll find that missing ssr-app-router string.

This is my understanding: The JavaScript is run on the server and it renders components. Those components are described by the RSC payload so that React can render them on the client. This sounds redundant, but the idea is that the client rendering should be super fast since all calculations have already been done. The RSC payload is an object with fields for children and attributes that are already determined, it's not the code needed to generate that object.

The upshot is that some data is sent to the client twice. In my simple example, a <div> with a data attribute and some text, 'Hello from the server', is sent both as HTML and as the content of a RSC payload that looks roughtly like this:

<!-- HTML -->
<div data-id="ssr-app-router">Hello from the server!</div>
// One portion of the RSC Payload (simplified for the example)
"5": // This is an identifier for this piece of the payload
  [
    "$", // the dollar sign indicates that this is part of a server component
    "div",
    null, // this is the react key, if necessary (i.e. the key in an <li> element)
    {
    "data-id": "ssr-app-router",
    "children": "Hello from the server!"
    }
  ]

Some people criticize this approach for being inefficient. It does add bulk to the size of the html document, even more than doubling it. I built an app with the example components above and compared the size of the generated html pages:

RSC in App Router    index.html:    4982 bytes
Pages Router         index.html:    1633 bytes

So that's something to keep in mind when considering using Next. If you're building a content-focused website that doesn't have much interactivity — say, a news or reviews website — then it might not be worth the added bulk in your DOM. Or if you have a very interactive site — like Excalidraw or Figma — you might want to consider using a straight-up CSR app since you won't benefit as much from server rendering anyway.