How to Optimize RSC Payload Size

Learn how to use React Server Components efficiently in Next.js to reduce cost and improve performance
Last updated on February 26, 2025
Frameworks

Did you know that props passed from server to client components affect your resource usage?

Every time you forward a prop from the server to the client, it's sent as data over the network. Large objects result in more data transferred, and if you've worked with strictly client-rendered React, this may not be something you're used to.

In this post, we'll explain why React Server Components work this way, and demystifying the internal mechanism that makes it possible: serialization of data and promises across the network boundary.

The RSC payload is a serialized object representing the rendered result of a server component. They help the client by providing a set of instructions to render its own components, as opposed to making the client generate those instructions itself before it can render. For more context on the problems RSCs aim to solve, check out Understanding React Server Components.

React components are just functions. On the client, these functions take props and state as input, and use them to create DOM nodes. On the server, these functions are called RSCs–they take in data and create React code for the client to execute.

The client component is prerendered on the server if possible, so that a non-interactive version can be shown quickly. The RSC payload is used to reconcile the DOM between the server and the client components, and the client component then hydrates, making the relevant components interactive.

Let's return to the idea that a React component is a function. For a component to render on the client, it needs its props (the function's input) to be sent from the server. This is where RSC payloads come in: they are the mechanism by which data is transferred over the network.

The RSC payload includes:

  1. The rendered result of server components
  2. Placeholders to indicate where a client component should be rendered, with a reference to the JavaScript chunk containing the client code
  3. Props passed from server components to client components

Let's look at each of these more closely and discuss how they contribute to data transfer usage.

This part of the RSC is pretty intuitive: when you generate a component on the server, it needs to be sent to the client to be shown in the browser. If you generate a large page, for example, it’s going to contain more data, resulting in a larger RSC payload.

This part of the RSC payload has a bit more nuance. While the reference system used by RSCs is efficient, there are a couple main patterns that can increase payload size:

  • Importing a large number of unique client components in a server component
  • Importing a very deep client component tree in a server component

The solution to both of these is to refactor with the goal of reducing the number of unique components included in the RSC payload. This allows client components to be referenced when they are reused on the page, as opposed to providing a full definition for each component.

If your client component tree is deep, consider whether some of your client components can be rendered on the server. While you cannot import a server component into a client component, you can pass it as a prop such as children.

Let’s make this clearer with an example. Suppose you have this server component:

'use client'
export function Modal({ largeObject }) {
const [open, setOpen] = useState(false)
return (
<div>
<button onClick={() => setOpen(true)}>Show</button>
{open && <Content largeObject={largeObject} />}
</div>
)
}

The large Content component will be rendered on the client because it’s part of the client component’s render tree. The way to prevent this is by passing large components that can be rendered on the server as children:

// Client component
'use client'
export function ModalWrapper({ children }) {
const [open, setOpen] = useState(false)
return (
<div>
<button onClick={() => setOpen(true)}>Show</button>
{open && children}
</div>
)
}
// Server component
export async function Page() {
const data = await fetchContentData()
return (
<ModalWrapper> // The client component is a wrapper
<Content largeObject={data} /> // Fully rendered on the server
</ModalWrapper>
)
}

The difference is that children is a prop (you can do this with any prop, not just children). Client components can’t import server components, but when they are rendered in a server component, they can take in other server components as props. Think of it this way:

  • If a client component imported a server component, how would it get that component’s code or rendered output? It would need to make a network request back to the server, which could block its own rendering.
  • By passing a server component as a prop, it can still be imported and rendered on the server. This means the RSC payload includes its rendered output, which is usually smaller than the raw object used to render it on the client.

For more examples, refer to our doc on interleaving server and client components.

This surprises some people. In some cases, you might see an increase in data transfer that you can’t trace back to a particular static file, and from there, it can be hard to figure out the source.

In these situations, the @next/bundle-analyzer package can be incredibly helpful. Read more about it in this doc.

We are continuing to automatically optimize the RSC payload inside Next.js applications. Here are some best practices to consider when rendering client components.

Only pass the data you need from server to client components. If your API returns a large object that you want to display, filter unused properties before passing it to the client.

If you are fetching blog posts, for example, the length of a user’s posts could contribute to your data transfer costs. Be sure to normalize this data, or even better, avoid passing it as a prop to a client component at all by interleaving server and client components.

The way to solve the client-side localization problem may not be obvious—suppose you have the following page as a server component:

// app/[locale]/page.tsx
const translations = {
es: {
title: 'Bienvenido a mi blog',
description: 'Este es un ejemplo de internacionalización con Suspense'
},
en: {
title: 'Welcome to my blog',
description: 'This is an example of internationalization with Suspense'
}
}
async function fetchTranslations(locale: string) {
// Simulate fetching from an API or file
await new Promise(resolve => setTimeout(resolve, 3000))
switch (locale) {
case 'es':
return translations.es
default:
return translations.en
}
}
export default function Home({ params: { locale } }: { params: { locale: string } }) {
const translationsPromise = fetchTranslations(locale)
return (
<main>
<h1>My Internationalized Blog</h1>
<Suspense fallback={<div>Loading content...</div>}>
<LocalizedContent translationsPromise={translationsPromise} />
</Suspense>
</main>
)
}

…and the following client component that uses localized content :

// app/[locale]/LocalizedContent.tsx
'use client'
export default function LocalizedContent({
translationsPromise
}: {
translationsPromise: Promise<{ title: string; description: string }>
}) {
const translations = use(translationsPromise)
return (
<div>
<h2>{translations.title}</h2>
<p>{translations.description}</p>
</div>
)
}

Now suppose a user requests the /es localized page. The server renders its component tree as HTML, including the fallback for the client component inside the Suspense boundary. An RSC payload is sent to hydrate the client components, but note that it does not include any translation data…yet.

This is because the Promise returned by the “fetch” is still pending. The use hook recognizes this and causes the fallback to be rendered while the client component is suspended.

While this happens, the server continues to process the request in the background, and streams it to the client when it’s ready. Our use hook then allows the suspended client component to render its translated content when the Promise resolves.

Sound complicated? Consider this: if you need to handle locale switching on the client, the alternative is to send all translation data in the initial RSC payload. Splitting up the work this way is more efficient because you only receive what you need, when you need it. If you support, say, 10 different languages, this means you could potentially transfer 90% less data.

When you render many instances of one component - a grid of product cards, for example - use pagination or infinite scroll, loading in batches so that you don’t send more data than you need at once.

Lazy load components you don’t strictly need to be displayed on the initial load, i.e. anything outside the viewport or below the fold. This prevents the components themselves from being sent in the initial bundle. Instead, it includes a reference that allows the client to display a fallback while it waits for the server to stream the data that will allow it to resolve. This does not always transfer less total data per page, but it can improve performance on the initial load. Additionally, it’s likely that some users will never scroll far enough to load it, assuming you trigger the loading with an intersection observer. Note that the Next.js Link component does this by default when prefetching pages.

Push client components “down” in your component tree; this means your root layout and other components higher up should not be client-rendered.

This is closely related to the idea that server components can be interleaved with client components. The rule of thumb: if you don’t need to render it on the client, don’t render it on the client.

A quick way to check for obvious waste is to run a local build with npm run build and then sort the statically generated RSCs by size:

find .next -type f -name "*.rsc" -exec du -h {} + | sort -nr | head -n 5

The output will include your top 5 heaviest RSC payloads. The output should resemble:

64K .next/server/app/large-object.rsc
28K .next/server/app/large-i18n.rsc
12K .next/server/app/normal-object.rsc
8.0K .next/server/app/normal-i18n.rsc
8.0K .next/server/app/index.rsc

There is no magic number at which the RSC is “too big” but if you notice outliers in this list, they might be good candidates for optimization. You can change the number passed to head to include more or fewer results.

It’s important to understand that RSCs are not only generated at build time. They are also used on dynamic pages and when an ISR function regenerates a static page. Keeping the values in this list low does not guarantee that every RSC payload will be small and efficient.

In dynamic cases, diagnosing the issue is harder because it is specific to your data. A few ways to investigate:

  • Use the network tab in the browser’s dev tools to look for large requests.
  • In some cases, like link prefetching, you can find RSC payloads by looking for the ?_rsc=... query parameter.
  • If you find an RSC that seems large, use a tool like RSC Parser to investigate further.
  • Use Observability to periodically check for unexpected increases in usage.
  • Check the size of potential large objects before passing them to client components, logging them if they’re larger than a certain threshold
  • One way to estimate size is to use JSON.stringify(yourObject).length - just be sure that your object is serializable if you use this method. Note that this is a simplified method and may not be exactly the same as the final serialized value included in the payload.

Now that you understand RSCs and how their payloads contribute to page size and data transfer usage, you can begin putting this into action as you build.

The takeaway here is not to avoid client components or to work around them whenever possible. Just be sure to understand the mechanics of your application and what it’s doing. React was initially created as a library for client-side interactions, and it is still a great option for creating frontends with an interactive user experience.

I’ll end by reiterating the core message: it’s not about avoiding bills or slow apps, it’s about understanding what your code is doing and being thoughtful about using the right tools for the job.

Couldn't find the guide you need?