If you want better results with next.js rsc payload size, this guide explains the practical steps, common mistakes, and useful browser-based tools that make the process easier.
We have an HTML sitemap page that lists every tool and article on the site. It renders about 370 links.
It was answering 2,318,515 bytes. Nothing on the page is an image, there is no embedded media, and the markup itself accounts for less than a fifth of that.
The rest was data we had already sent, serialized a second time into the HTML.
Quick Takeaways
- Focus first on the mechanism: props cross the boundary as text.
- Apply the steps from this guide to improve next.js rsc payload size without overcomplicating the workflow.
- Use HTML Minifier to turn this advice into action directly in your browser.
- Read Migrating pdf.js 3 to 5: The Breaking Changes Nobody Warns You About if you want a related guide that expands on the same topic.
Pro Tip
Want a faster path?
Start with HTML Minifier and then continue with Migrating pdf.js 3 to 5: The Breaking Changes Nobody Warns You About to build a practical workflow around next.js rsc payload size.
This is a property of the React Server Components architecture that is easy to read past in the docs and very hard to notice in practice, because the page looks perfect and every build, type check and lint run stays green.
Here is the mechanism, the numbers before and after, and the measuring error that first told us there was no problem at all.
The Mechanism: Props Cross the Boundary as Text
In the App Router, a Server Component can render a Client Component and pass it props. Those props have to reach the browser so React can hydrate, and the way they travel is the RSC flight payload:
a series of inline scripts in the HTML of the form self.__next_f.push([...]). Every field of every object you pass is written into that payload verbatim.
The important word is verbatim. The payload is not a diff, it is not compressed relative to the markup, and it does not contain only the fields the component reads.
If you hand a Client Component an array of 255 objects with fifteen fields each, all 255 objects and all fifteen fields are serialized, even if the component displays three of them.
So the data is on the page twice: once as the rendered markup, and once as JSON for hydration. That is the intended design and it is not a bug. The bug is passing more than the component needs.
Warning
This is separate from your JavaScript bundle.
A bundle budget measures client chunks; the flight payload is inline HTML.
We had a bundle gate in CI the whole time and it never moved, because the dependency graph genuinely did not change.
Nothing we had was looking at this.
What We Actually Found
If you would rather do this step in the browser than by hand, SEO Checker handles it without a signup.
Measuring the payload by field name told the story immediately. On that 2.32 MB page, counting occurrences of each key inside the flight scripts:
- description appeared 522 times
- keywords appeared 294 times
- faqs appeared 102 times
- excerpt appeared 101 times
- trending appeared 510 times
- longDescription appeared 70 times
The component reads none of those. It renders a link, a label, and a small badge.
Every one of those fields was shipped to every visitor and every crawler because the page handed the component whole catalogue objects instead of the four fields it uses.
The sharpest single finding was an array we were passing purely so the component could render its length. The component never mapped it.
It called .length in four places to print a total. To print one number we were serializing 255 complete objects into the HTML.
Pro Tip
Before optimising anything, grep the component for what it actually reads off each object.
Ours came to slug, name, popular for a tool and slug, title, category for an article.
That list is the prop shape.
The Fix: Project Before the Client Boundary
Website Speed Test is the quickest way to apply what this section describes to your own file.
The fix is not clever. In the Server Component, map the data down to the fields the Client Component reads, and pass that. The array that only produced a count becomes a number.
- Narrow each object to the fields the component reads, in the Server Component, before the boundary.
- Replace any array you pass only to call .length on with the count itself.
- Type the narrow shapes as Pick<Model, 'a' | 'b'> rather than hand writing them, so a field renamed upstream fails the build instead of silently arriving undefined.
- Keep every element. Narrow the FIELDS, not the array, or you change counts the page displays.
That last point is worth stating because it is the easy way to break something while making a page smaller. Our component prints the number of articles from the array it receives and then slices it for display.
Trimming the array on the server would have silently changed a number a reader sees.
The Numbers
We measured the same page on one dev server, before and after, so the only variable was the projection. Comparing a production build against a dev build would have proved nothing.
- Total HTML: 1,867,283 bytes down to 611,003 bytes, a 67.3% reduction.
- Flight payload: 1,453,764 bytes down to 197,484 bytes, an 86.4% reduction.
- Rendered markup: 410,348 bytes both times, byte for byte identical.
- Links rendered: 255 tool links, 90 article links, 10 category links, identical.
The byte identical markup is the part that matters. It is the proof that the change moved weight and not output.
On the production build the effect was larger still: 2,318,515 bytes down to 556,662, a 76% reduction, with time to first byte going from 2.23 seconds to about 0.57 seconds across five samples.
Why We Cared: Crawl Budget, Not Lighthouse
Worth keeping open alongside this guide: JSON Formatter, which covers the same job in a couple of clicks.
The reason we went looking was not a performance score. It was a Search Console crawl stats export.
Over a 90 day window Google made 2,570 requests to the site, of which only 274 were HTML, against 406 URLs in the sitemap. That is fewer than 0.7 HTML fetches per URL per quarter.
Google's own guidance makes crawl rate a function of host response time. The page in question was linked from the footer of roughly 400 pages, so a crawler meets it constantly,
and it was both the heaviest URL on the site by a factor of three and the slowest we measured. Every second spent serving it is paid for out of some other page's chance of being fetched at all.
If your site is small and well linked, this argument does not apply to you and you can treat the payload as a pure performance question.
If you are struggling to get pages indexed, it is worth knowing that your heaviest URL is competing with your most important one.
How to Measure It Yourself
The flight payload is every script tag of the form self.__next_f.push(...). Fetch a page, sum the byte length of those blocks, and compare against the total.
That gives you the share. To find out which fields are responsible, count occurrences of each key name inside those blocks.
A rough reading for a single page, in Node:
- Fetch the page as plain text. Do not use a headless browser; you want the bytes on the wire.
- Match /<script>self\.__next_f\.push\(([\s\S]*?)\)<\/script>/g and sum Buffer.byteLength of each full match.
- Divide by Buffer.byteLength of the whole document to get the share.
- For the field breakdown, count occurrences of each candidate key inside the joined matches.
Anything above about half the page is worth a look. Ours came in at 82.7%, while other pages on the same site measured 21% to 51% and turned out to be passing slim references already. A high share is a signal, not a verdict.
The Measuring Mistake That Nearly Ended This Investigation
For the longer version of this point, read The Complete Guide to JSON Formatting and Validation for Developers.
We made a related measurement in the same session and got it confidently wrong, and the mistake generalises, so it is worth spelling out.
React streaming SSR also parks page content inside a hidden container while the shell flushes. We wanted to know how much of a page sits inside one, so we stripped hidden containers with a regex and counted what was left.
The regex was the obvious one: match a div with a hidden attribute, then everything up to the next closing div tag, non greedy.
Warning
A non greedy match terminates at the FIRST nested closing tag, not the matching one.
On real markup it therefore strips almost nothing.
It told us 99.5% of the content was visible.
The true figure, measured with a depth counter, was 0.5%.
We had the answer exactly inverted and it looked entirely plausible.
HTML is not a regular language and nested elements of the same tag name are precisely where that bites. If you are measuring anything that depends on element nesting, walk the tags with a depth counter or use a real parser.
A regex will give you a number, and the number will be wrong in the reassuring direction.
What to Take Away
- Every field you pass to a Client Component is serialized into the HTML, on top of being rendered. Pass the fields it reads and nothing else.
- Never pass an array you only need a count from. Pass the count.
- Your bundle budget cannot see any of this. The flight payload is inline HTML, not a chunk.
- Measure before and after on the same server. A production versus dev comparison is not an isolation.
- Byte identical rendered markup is the cleanest proof that a size change did not change behaviour.
- If you strip nested elements with a regex, assume the result is wrong until a depth aware count agrees with it.
None of this required a rewrite. It was one projection in one Server Component, and the page still renders exactly the same markup it did before.
The easiest way to improve next.js rsc payload size is to follow a repeatable checklist, test the result, and use the right tool for the specific task instead of forcing one workflow on every use case.
For official background, standards, or platform guidance, review Next.js Server Components documentation.
Continue Reading on ToolsMonk
Explore related guides that build on this topic and help you go deeper into Next.js RSC Payload Size.
Useful External References
These authoritative resources add context, standards, or official guidance related to this topic.
Tools Mentioned in This Article
Frequently Asked Questions
Common questions readers ask about this topic and the tools connected to it.
Developer Desk · ToolsMonk
The Developer Desk is the engineering team that builds ToolsMonk's developer utilities, JSON, regex, encoding, hashing, formatters, and converters. These guides are written by the engineers who implement the tools, so the explanations of formats, algorithms, and edge cases come from building them, not just describing them. Every guide is researched, written, and reviewed by the same team that designs and maintains the underlying ToolsMonk tools, then fact-checked against primary sources and updated as standards change.
View all posts by ToolsMonk Developer Desk →