Skip to content
Back to blog
Web Engineering22 August 20266 min read

RSC Payload Budgets for App Router Pages

How to cap React Server Component flight size so list pages stop shipping unused query fields to the browser.

App Router page with a measured RSC payload budget and a narrow query selector.

Kabir Hossain

Founder, Chainweb Solutions

View profile
Next.jsReact Server ComponentsApp RouterPerformance

RSC Payload Budgets for App Router Pages

A React Server Component can read the database. That does not mean the result should ride the flight payload to the browser.

We started measuring serialized RSC payload size after a dashboard page passed Lighthouse and still felt heavy on mid-range phones. The HTML looked fine. The RSC payload was 180 kb of rows the page never rendered.

The server fetch is not free just because it is on the server

App Router pages often load a wide query in the page Server Component, then pass a slice into a child. The unused fields still serialize. Nested objects, decimals, and ISO timestamps add up faster than the UI suggests.

We set a budget: 24 kb uncompressed for the RSC payload on any authenticated list page, 40 kb on a detail page. Above that, the page is doing work the client should never see.

The measurement is the flight payload, not the HTML document. If you only watch transfer size for the document request, you miss the follow-on RSC traffic after a client navigation.

Select for the component, not for the table

Two common approaches. Fetch the full record and pick fields in JSX. Or write a query that returns only the fields that component renders.

The first is faster to write and slower forever. The second needs a typed selector and breaks when a designer adds a column. We accept that breakage. A missing field in development is cheaper than shipping every invoice line to a list view.

On one Next.js project the order list selected SELECT * through an ORM default. Switching to an explicit list of eight columns cut the payload from 180 kb to 22 kb. Time-to-interactive on a throttled 4G profile dropped by about 400 ms. The query plan barely changed. The serialization did.

Children should ask, parents should not preload the world

It is tempting to fetch everything in page.tsx so children stay pure. That inverts the cost. The parent becomes a dump truck.

We let leaf Server Components fetch their own narrow data when the data is unique to that leaf. Shared data — the current user, the tenant, the nav badge — stays in the layout. List rows stay in the list component.

The tradeoff is more queries. That is acceptable when each query is small and cacheable. It is not acceptable when a leaf runs an uncached aggregation on every request. Put that aggregation behind a tagged cache or a background job.

A failure mode with dates and decimals

One payload looked small in row count and huge on the wire. The ORM serialized every Date as a full ISO string and every money value as a stringified decimal object with metadata. Forty rows became 90 kb.

The mitigation was a boundary mapper: dates as YYYY-MM-DD, money as integer cents, no ORM class instances past the query function. The mapper lives next to the selector. If a field is not in the mapper, it does not ship.

How we catch regressions

A CI check records RSC payload size for the five heaviest routes against a committed baseline. A 15 percent increase fails the build. The check runs in the Node test environment against fixtures, not against production data.

We also log payload bytes in staging on every document request. If a single tenant blows the budget because they have 10x the rows, that is a pagination bug, not a reason to raise the budget.

Final takeaway

Give every App Router page an RSC payload budget. Select only what the component renders, and fail the build when a query starts shipping the rest of the table.

Related articles

Continue with articles on similar topics.