Skip to content

[Fizz] Enable the progressiveChunkSize option #33027

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 25, 2025

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Apr 25, 2025

Since the very beginning we have had the progressiveChunkSize option but we never actually took advantage of it because we didn't count the bytes that we emitted. This starts counting the bytes by taking a pass over the added chunks each time a segment completes.

That allows us to outline a Suspense boundary to stream in late even if it is already loaded by the time that back-pressure flow and in a prerender. Meaning it gets inserted with script.

The effect can be seen in the fixture where if you have large HTML content that can block initial paint (thanks to rel="expect" but also nested Suspense boundaries). Before this fix, the paint would be blocked until the large content loaded. This lets us paint the fallback first in the case that the raw bytes of the content takes a while to download.

You can set it to Infinity to opt-out. E.g. if you want to ensure there's never any scripts. It's always set to Infinity in renderToHTML and the legacy renderToString.

One downside is that if we might choose to outline a boundary, we need to let its fallback complete.

We don't currently discount the size of the fallback but really just consider them additive even though in theory the fallback itself could also add significant size or even more than the content. It should maybe really be considered the delta but that would require us to track the size of the fallback separately which is tricky.

One problem with the current heuristic is that we just consider the size of the boundary content itself down to the next boundary. If you have a lot of small boundaries adding up, it'll never kick in. I intend to address that in a follow up.

@sebmarkbage sebmarkbage requested a review from gnoff April 25, 2025 18:18
@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label Apr 25, 2025
@react-sizebot
Copy link

Comparing: 143d3e1...111f72d

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.68 kB 6.68 kB = 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 527.72 kB 527.72 kB = 93.07 kB 93.07 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB = 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 633.34 kB 633.34 kB = 111.25 kB 111.25 kB
facebook-www/ReactDOM-prod.classic.js = 671.13 kB 671.13 kB = 117.70 kB 117.70 kB
facebook-www/ReactDOM-prod.modern.js = 661.41 kB 661.41 kB = 116.14 kB 116.14 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable-semver/react-server/cjs/react-server.production.js +0.85% 116.17 kB 117.16 kB +0.67% 20.73 kB 20.87 kB
oss-stable/react-server/cjs/react-server.production.js +0.85% 116.17 kB 117.16 kB +0.67% 20.73 kB 20.87 kB
oss-experimental/react-server/cjs/react-server.production.js +0.78% 130.22 kB 131.23 kB +0.69% 22.61 kB 22.76 kB
oss-stable-semver/react-server/cjs/react-server.development.js +0.60% 172.64 kB 173.68 kB +0.48% 31.03 kB 31.18 kB
oss-stable/react-server/cjs/react-server.development.js +0.60% 172.64 kB 173.68 kB +0.48% 31.03 kB 31.18 kB
oss-experimental/react-server/cjs/react-server.development.js +0.56% 189.34 kB 190.41 kB +0.49% 33.07 kB 33.23 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-server.development.js +0.43% 7.63 kB 7.66 kB +0.86% 1.63 kB 1.64 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-server.development.js +0.43% 7.63 kB 7.66 kB +0.86% 1.63 kB 1.64 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-server.development.js +0.43% 7.63 kB 7.66 kB +0.86% 1.63 kB 1.64 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-server.production.js +0.43% 6.34 kB 6.37 kB +0.84% 1.55 kB 1.56 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-server.production.js +0.43% 6.34 kB 6.37 kB +0.84% 1.55 kB 1.56 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-server.production.js +0.43% 6.34 kB 6.37 kB +0.84% 1.55 kB 1.56 kB
oss-stable-semver/react-dom/cjs/react-dom-server.bun.production.js +0.42% 218.19 kB 219.12 kB +0.37% 40.05 kB 40.20 kB
oss-stable/react-dom/cjs/react-dom-server.bun.production.js +0.42% 218.27 kB 219.19 kB +0.36% 40.08 kB 40.23 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.production.js +0.42% 238.57 kB 239.58 kB +0.39% 42.82 kB 42.99 kB
oss-stable-semver/react-dom/cjs/react-dom-server.node.production.js +0.42% 234.72 kB 235.70 kB +0.35% 42.63 kB 42.78 kB
oss-stable/react-dom/cjs/react-dom-server.node.production.js +0.42% 234.80 kB 235.78 kB +0.35% 42.66 kB 42.81 kB
oss-experimental/react-dom/cjs/react-dom-server.node.production.js +0.40% 262.34 kB 263.40 kB +0.38% 46.20 kB 46.38 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.production.js +0.39% 232.96 kB 233.86 kB +0.30% 41.69 kB 41.81 kB
oss-stable/react-dom/cjs/react-dom-server.browser.production.js +0.39% 233.03 kB 233.93 kB +0.30% 41.71 kB 41.84 kB
oss-stable-semver/react-dom/cjs/react-dom-server.edge.production.js +0.38% 238.16 kB 239.06 kB +0.29% 43.62 kB 43.75 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.production.js +0.38% 260.47 kB 261.45 kB +0.36% 45.07 kB 45.23 kB
oss-stable/react-dom/cjs/react-dom-server.edge.production.js +0.38% 238.24 kB 239.13 kB +0.29% 43.65 kB 43.78 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.production.js +0.37% 266.42 kB 267.40 kB +0.34% 47.21 kB 47.37 kB
oss-stable-semver/react-dom/cjs/react-dom-server.bun.development.js +0.32% 325.25 kB 326.29 kB +0.30% 63.17 kB 63.36 kB
oss-stable/react-dom/cjs/react-dom-server.bun.development.js +0.32% 325.33 kB 326.37 kB +0.30% 63.20 kB 63.38 kB
oss-stable-semver/react-dom/cjs/react-dom-server.node.development.js +0.30% 377.99 kB 379.14 kB +0.28% 67.87 kB 68.06 kB
oss-stable/react-dom/cjs/react-dom-server.node.development.js +0.30% 378.07 kB 379.22 kB +0.28% 67.93 kB 68.11 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.development.js +0.30% 350.01 kB 351.08 kB +0.34% 66.45 kB 66.68 kB
oss-experimental/react-dom/cjs/react-dom-server.node.development.js +0.28% 413.42 kB 414.60 kB +0.28% 71.92 kB 72.13 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.development.js +0.28% 381.54 kB 382.60 kB +0.25% 68.52 kB 68.69 kB
oss-stable/react-dom/cjs/react-dom-server.browser.development.js +0.28% 381.61 kB 382.68 kB +0.25% 68.57 kB 68.74 kB
oss-stable-semver/react-dom/cjs/react-dom-server.edge.development.js +0.28% 382.32 kB 383.38 kB +0.25% 68.66 kB 68.84 kB
oss-stable/react-dom/cjs/react-dom-server.edge.development.js +0.28% 382.39 kB 383.46 kB +0.25% 68.71 kB 68.89 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.development.js +0.26% 417.50 kB 418.59 kB +0.27% 72.54 kB 72.73 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.development.js +0.26% 418.51 kB 419.60 kB +0.27% 72.75 kB 72.94 kB

Generated by 🚫 dangerJS against 111f72d

@@ -348,6 +349,7 @@ export opaque type Request = {
pendingRootTasks: number, // when this reaches zero, we've finished at least the root boundary.
completedRootSegment: null | Segment, // Completed but not yet flushed root segments.
completedPreambleSegments: null | Array<Array<Segment>>, // contains the ready-to-flush segments that make up the preamble
byteSize: number, // counts the number of bytes accumulated in the shell
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems this isn't doing anything for now. But your comment didn't indicate that you have plans for tracking the size of the root. I suppose if the root was tiny we ought to still include even large boundaries because there isn't anything else being blocked that is meaningful?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using it in #33029

Copy link

@Hardanish-Singh Hardanish-Singh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@sebmarkbage sebmarkbage merged commit 8e9a5fc into facebook:main Apr 25, 2025
245 checks passed
github-actions bot pushed a commit that referenced this pull request Apr 25, 2025
Since the very beginning we have had the `progressiveChunkSize` option
but we never actually took advantage of it because we didn't count the
bytes that we emitted. This starts counting the bytes by taking a pass
over the added chunks each time a segment completes.

That allows us to outline a Suspense boundary to stream in late even if
it is already loaded by the time that back-pressure flow and in a
`prerender`. Meaning it gets inserted with script.

The effect can be seen in the fixture where if you have large HTML
content that can block initial paint (thanks to
[`rel="expect"`](#33016) but also
nested Suspense boundaries). Before this fix, the paint would be blocked
until the large content loaded. This lets us paint the fallback first in
the case that the raw bytes of the content takes a while to download.

You can set it to `Infinity` to opt-out. E.g. if you want to ensure
there's never any scripts. It's always set to `Infinity` in
`renderToHTML` and the legacy `renderToString`.

One downside is that if we might choose to outline a boundary, we need
to let its fallback complete.

We don't currently discount the size of the fallback but really just
consider them additive even though in theory the fallback itself could
also add significant size or even more than the content. It should maybe
really be considered the delta but that would require us to track the
size of the fallback separately which is tricky.

One problem with the current heuristic is that we just consider the size
of the boundary content itself down to the next boundary. If you have a
lot of small boundaries adding up, it'll never kick in. I intend to
address that in a follow up.

DiffTrain build for [8e9a5fc](8e9a5fc)
github-actions bot pushed a commit to code/lib-react that referenced this pull request Apr 26, 2025
Since the very beginning we have had the `progressiveChunkSize` option
but we never actually took advantage of it because we didn't count the
bytes that we emitted. This starts counting the bytes by taking a pass
over the added chunks each time a segment completes.

That allows us to outline a Suspense boundary to stream in late even if
it is already loaded by the time that back-pressure flow and in a
`prerender`. Meaning it gets inserted with script.

The effect can be seen in the fixture where if you have large HTML
content that can block initial paint (thanks to
[`rel="expect"`](facebook#33016) but also
nested Suspense boundaries). Before this fix, the paint would be blocked
until the large content loaded. This lets us paint the fallback first in
the case that the raw bytes of the content takes a while to download.

You can set it to `Infinity` to opt-out. E.g. if you want to ensure
there's never any scripts. It's always set to `Infinity` in
`renderToHTML` and the legacy `renderToString`.

One downside is that if we might choose to outline a boundary, we need
to let its fallback complete.

We don't currently discount the size of the fallback but really just
consider them additive even though in theory the fallback itself could
also add significant size or even more than the content. It should maybe
really be considered the delta but that would require us to track the
size of the fallback separately which is tricky.

One problem with the current heuristic is that we just consider the size
of the boundary content itself down to the next boundary. If you have a
lot of small boundaries adding up, it'll never kick in. I intend to
address that in a follow up.

DiffTrain build for [8e9a5fc](facebook@8e9a5fc)
sebmarkbage added a commit that referenced this pull request Apr 29, 2025
…pletion (#33029)

Follow up to #33027.

This enhances the heuristic so that we accumulate the size of the
currently written boundaries. Starting from the size of the root (minus
preamble) for the shell.

This ensures that if you have many small boundaries they don't all
continue to get inlined. For example, you can wrap each paragraph in a
document in a Suspense boundary to regain document streaming
capabilities if that's what you want.

However, one consideration is if it's worth producing a fallback at all.
Maybe if it's like `null` it's free but if it's like a whole alternative
page, then it's not. It's possible to have completely useless Suspense
boundaries such as when you nest several directly inside each other. So
this uses a limit of at least 500 bytes of the content itself for it to
be worth outlining at all. It also can't be too small because then for
example a long list of paragraphs can never be outlined.

In the fixture I straddle this limit so some paragraphs are too small to
be considered. An unfortunate effect of that is that you can end up with
some of them not being outlined which means that they appear out of
order. SuspenseList is supposed to address that but it's unfortunate.

The limit is still fairly high though so it's unlikely that by default
you'd start outlining anything within the viewport at all. I had to
reduce the `progressiveChunkSize` by an order of magnitude in my fixture
to try it out properly.
github-actions bot pushed a commit that referenced this pull request Apr 29, 2025
…pletion (#33029)

Follow up to #33027.

This enhances the heuristic so that we accumulate the size of the
currently written boundaries. Starting from the size of the root (minus
preamble) for the shell.

This ensures that if you have many small boundaries they don't all
continue to get inlined. For example, you can wrap each paragraph in a
document in a Suspense boundary to regain document streaming
capabilities if that's what you want.

However, one consideration is if it's worth producing a fallback at all.
Maybe if it's like `null` it's free but if it's like a whole alternative
page, then it's not. It's possible to have completely useless Suspense
boundaries such as when you nest several directly inside each other. So
this uses a limit of at least 500 bytes of the content itself for it to
be worth outlining at all. It also can't be too small because then for
example a long list of paragraphs can never be outlined.

In the fixture I straddle this limit so some paragraphs are too small to
be considered. An unfortunate effect of that is that you can end up with
some of them not being outlined which means that they appear out of
order. SuspenseList is supposed to address that but it's unfortunate.

The limit is still fairly high though so it's unlikely that by default
you'd start outlining anything within the viewport at all. I had to
reduce the `progressiveChunkSize` by an order of magnitude in my fixture
to try it out properly.

DiffTrain build for [18212ca](18212ca)
github-actions bot pushed a commit to code/lib-react that referenced this pull request Apr 30, 2025
…pletion (facebook#33029)

Follow up to facebook#33027.

This enhances the heuristic so that we accumulate the size of the
currently written boundaries. Starting from the size of the root (minus
preamble) for the shell.

This ensures that if you have many small boundaries they don't all
continue to get inlined. For example, you can wrap each paragraph in a
document in a Suspense boundary to regain document streaming
capabilities if that's what you want.

However, one consideration is if it's worth producing a fallback at all.
Maybe if it's like `null` it's free but if it's like a whole alternative
page, then it's not. It's possible to have completely useless Suspense
boundaries such as when you nest several directly inside each other. So
this uses a limit of at least 500 bytes of the content itself for it to
be worth outlining at all. It also can't be too small because then for
example a long list of paragraphs can never be outlined.

In the fixture I straddle this limit so some paragraphs are too small to
be considered. An unfortunate effect of that is that you can end up with
some of them not being outlined which means that they appear out of
order. SuspenseList is supposed to address that but it's unfortunate.

The limit is still fairly high though so it's unlikely that by default
you'd start outlining anything within the viewport at all. I had to
reduce the `progressiveChunkSize` by an order of magnitude in my fixture
to try it out properly.

DiffTrain build for [18212ca](facebook@18212ca)
github-actions bot pushed a commit to code/lib-react that referenced this pull request Apr 30, 2025
…pletion (facebook#33029)

Follow up to facebook#33027.

This enhances the heuristic so that we accumulate the size of the
currently written boundaries. Starting from the size of the root (minus
preamble) for the shell.

This ensures that if you have many small boundaries they don't all
continue to get inlined. For example, you can wrap each paragraph in a
document in a Suspense boundary to regain document streaming
capabilities if that's what you want.

However, one consideration is if it's worth producing a fallback at all.
Maybe if it's like `null` it's free but if it's like a whole alternative
page, then it's not. It's possible to have completely useless Suspense
boundaries such as when you nest several directly inside each other. So
this uses a limit of at least 500 bytes of the content itself for it to
be worth outlining at all. It also can't be too small because then for
example a long list of paragraphs can never be outlined.

In the fixture I straddle this limit so some paragraphs are too small to
be considered. An unfortunate effect of that is that you can end up with
some of them not being outlined which means that they appear out of
order. SuspenseList is supposed to address that but it's unfortunate.

The limit is still fairly high though so it's unlikely that by default
you'd start outlining anything within the viewport at all. I had to
reduce the `progressiveChunkSize` by an order of magnitude in my fixture
to try it out properly.

DiffTrain build for [18212ca](facebook@18212ca)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants