Skip to content

Bug: Performance regression due to deferTask not batching #35125

@mhart

Description

@mhart

Overview

Pulling this out of #35089 (read this for background)

As mentioned above, #33030 introduced a fixed MAX_ROW_SIZE=3200, above which elements are deferred, which introduced a performance regression for certain payloads.

A large part of the issue appears to be that the children aren't deferred in batches, they're deferred individually. This can result in many rows that are far smaller than the MAX_ROW_SIZE – because renderModelDestructive will call deferTask on each child, which then becomes its own lazy chunk, until all children have finished being serialized, and the serializedSize is reset again.

Example

I've created a small reproduction that illustrates the problem. It's a ~120kb page with 20 sections each containing ~100 paragraphs. It can be made roughly 1.75x faster by better batching.

Using plain React and renderToReadableStream this page renders in 1.02ms on Bun, and 1.27ms on Node.js. In Next.js (16.0.3) it's roughly 15x slower with the current batching strategy.

This screenshot shows an example of what happens with the RSC stream: the first row reaches its limit after a few children, and starts deferring, but as you can see, each deferred child becomes its own row (a lazy chunk). So you can easily end up with hundreds or thousands of tiny rows if you're just synchronously rendering a table or similar.

Image

This example ends up with ~2000 rows, but each row is on average only 60 chars in length – far below the 3200 limit. And each row has non-trivial overhead as (in the Next.js case) it needs to serialized, de-serialized, and then re-serialized again (NB: this would be another optimization that would be good to tackle).

Increasing the MAX_ROW_SIZE is one way to reduce the number of rendered rows which is what #35089 does and produces significant results, but a better fix would be to allow these children to be batched together into rows of ~MAX_ROW_SIZE length.

That would obviate the need for #35089, or at least make it far less needed.

Thanks to @gnoff for chatting some of this through too.

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