Skip to content

RSC SSR performance #34727

@WIVSW

Description

@WIVSW

Hello!

I'm really like Server Components!

In my day job I do a lot of SSR and Server Components allows make it much simpler and faster.

But when I've switched from regular renderToPipeableStream to RSC SSR I've noticed a decrease in server throughput, from 329 to 29 RPS, and an increase in response timings. I've also noticed decrease in server throughput of flight request from 329 to 104 RPS in comparison to renderToPipeableStream.

Steps To Reproduce

I've create a demo repo which illustrates the issue by benchmarking RSC SSR on React 19.1.1, Node.js 22 and Parcel 2.15.4.

For benchmarking i'm using parcel SSR implementation because it short and straightforward but the issue presents in Next.js also. Parcel approach looks similar to this:

import {renderToReadableStream} from 'react-server-dom-parcel/server.edge';
import {injectRSCPayload} from 'rsc-html-stream/server';
import {createFromReadableStream} from 'react-server-dom-parcel/client.edge';
import {renderToReadableStream as renderHTMLToReadableStream} from 'react-dom/server.edge';

async function renderHTML(root: any): Promise<ReadableStream> {
  // serialize react tree to flight stream
  let rscStream = renderToReadableStream(root, options);

  // Use client react to render the RSC payload to HTML.
  let [s1, s2] = rscStream.tee();
  let data: Promise<ReactNode>;
  function Content() {
    // deserialize flight stream to react tree
    data ??= createFromReadableStream<ReactNode>(s1);
    return data;
  }

  // serialize flight react tree to HTML
  let htmlStream = await renderHTMLToReadableStream(<Content />);

  // inject JSON flight tree to html for hydration
  return htmlStream.pipeThrough(injectRSCPayload(s2));
}

In the benchmark server renders Stress component with 10,000 list items.

import React from 'react';

const count = 10_000;

const Stress = () => {
    const out = Array(count);

    for (let i = 0; i < count; i++) {
        out[i] = <li key={i}>{i}</li>
    }

    return <ul>{out}</ul>;
}

export default Stress;

The current behavior

If we try to record CPU Profile during request then we'll see that most of computing spent for JSON serialization and html rendering.

CPU Profile in production mode:
CPU-ssr-rsc-prod.cpuprofile
Image

CPU Profile in development mode:
CPU-rsc-ssr-dev.cpuprofile
Image

The expected behavior

I'm understand that JSON serialization is essential for RSC to work but I'm think that there is a optimization opportunities to discover:

  1. V8 team tells in blog article that JSON.stringify can run faster if it serializes only plain objects without custom prototype.toJSON() method. In theory refactoring could speed up flight serialization.
  2. In ReactFizzServer I saw a TODO comment that suppose injecting flight data for hydration directly in Fizz Stream. It might reduce amount of serialization in comparison to current RSC SSR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Status: UnconfirmedA potential issue that we haven't yet confirmed as a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions