Skip to content

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Sep 22, 2025

We should favor outlining a boundary if it contains Suspensey CSS or Suspensey Images since then we can load that content separately and not block the main content. This also allows us to animate the reveal.

For example this should be able to animate the reveal even though the actual HTML content isn't large in this case it's worth outlining so that the JS runtime can choose to animate this reveal.

<ViewTransition>
  <Suspense>
    <img src="..." />
  </Suspense>
</ViewTransition>

For Suspensey Images, in Fizz, we currently only implement the suspensey semantics when a View Transition is running. Therefore the outlining only applies if it appears inside a Suspense boundary which might animate. Otherwise there's no point in outlining. It is also only if the Suspense boundary itself might animate its appear and not just any ViewTransition. So the effect is very conservative.

For CSS it applies even without ViewTransition though, since it can help unblock the main content faster.

@sebmarkbage sebmarkbage requested a review from gnoff September 22, 2025 01:52
@meta-cla meta-cla bot added the CLA Signed label Sep 22, 2025
@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label Sep 22, 2025
@react-sizebot
Copy link

react-sizebot commented Sep 22, 2025

Comparing: 8ad773b...a9f2324

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 = 534.43 kB 534.43 kB = 94.33 kB 94.33 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 = 663.69 kB 663.69 kB = 117.00 kB 117.00 kB
facebook-www/ReactDOM-prod.classic.js = 687.59 kB 687.59 kB = 121.04 kB 121.04 kB
facebook-www/ReactDOM-prod.modern.js = 678.02 kB 678.02 kB = 119.39 kB 119.40 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-server.development.js +0.94% 7.95 kB 8.02 kB +0.84% 1.67 kB 1.69 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-server.development.js +0.94% 7.95 kB 8.02 kB +0.84% 1.67 kB 1.69 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-server.development.js +0.94% 7.95 kB 8.02 kB +0.84% 1.67 kB 1.69 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-server.production.js +0.86% 6.59 kB 6.65 kB +0.88% 1.59 kB 1.60 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-server.production.js +0.86% 6.59 kB 6.65 kB +0.88% 1.59 kB 1.60 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-server.production.js +0.86% 6.59 kB 6.65 kB +0.88% 1.59 kB 1.60 kB
oss-stable-semver/react-dom/cjs/react-dom-server.bun.production.js +0.34% 241.04 kB 241.87 kB +0.41% 43.95 kB 44.13 kB
oss-stable/react-dom/cjs/react-dom-server.bun.production.js +0.34% 241.12 kB 241.95 kB +0.41% 43.98 kB 44.16 kB
facebook-www/ReactDOMServerStreaming-prod.modern.js +0.34% 262.02 kB 262.90 kB +0.37% 47.88 kB 48.05 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.production.js +0.32% 257.08 kB 257.91 kB +0.39% 46.52 kB 46.70 kB
oss-stable/react-dom/cjs/react-dom-server.browser.production.js +0.32% 257.15 kB 257.99 kB +0.38% 46.55 kB 46.73 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.production.js +0.32% 275.61 kB 276.49 kB +0.38% 49.37 kB 49.56 kB
oss-stable-semver/react-dom/cjs/react-dom-server.edge.production.js +0.32% 262.39 kB 263.22 kB +0.35% 48.55 kB 48.72 kB
oss-stable/react-dom/cjs/react-dom-server.edge.production.js +0.32% 262.47 kB 263.30 kB +0.35% 48.58 kB 48.75 kB
oss-stable-semver/react-dom/cjs/react-dom-server.node.production.js +0.31% 262.63 kB 263.46 kB +0.36% 46.99 kB 47.16 kB
oss-stable/react-dom/cjs/react-dom-server.node.production.js +0.31% 262.71 kB 263.53 kB +0.36% 47.02 kB 47.19 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.production.js +0.30% 293.64 kB 294.53 kB +0.36% 51.36 kB 51.54 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.production.js +0.29% 299.98 kB 300.87 kB +0.36% 53.72 kB 53.91 kB
oss-stable-semver/react-server/cjs/react-server.production.js +0.29% 136.30 kB 136.69 kB +0.31% 24.06 kB 24.13 kB
oss-stable/react-server/cjs/react-server.production.js +0.29% 136.30 kB 136.69 kB +0.31% 24.06 kB 24.13 kB
oss-experimental/react-dom/cjs/react-dom-server.node.production.js +0.28% 307.86 kB 308.74 kB +0.33% 53.57 kB 53.75 kB
oss-experimental/react-server/cjs/react-server.production.js +0.26% 154.07 kB 154.46 kB +0.27% 26.71 kB 26.78 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.browser.production.js +0.25% 234.14 kB 234.73 kB +0.26% 42.42 kB 42.53 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.browser.production.js +0.25% 234.16 kB 234.76 kB +0.26% 42.45 kB 42.56 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.node.production.js +0.25% 238.75 kB 239.34 kB +0.25% 44.23 kB 44.34 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.node.production.js +0.25% 238.77 kB 239.37 kB +0.25% 44.26 kB 44.37 kB
facebook-www/ReactDOMServerStreaming-dev.modern.js +0.25% 406.44 kB 407.44 kB +0.26% 72.81 kB 73.00 kB
oss-stable-semver/react-dom/cjs/react-dom-server.bun.development.js +0.24% 350.25 kB 351.09 kB +0.26% 67.79 kB 67.96 kB
oss-stable/react-dom/cjs/react-dom-server.bun.development.js +0.24% 350.33 kB 351.17 kB +0.26% 67.82 kB 67.99 kB
oss-experimental/react-markup/cjs/react-markup.production.js +0.24% 251.61 kB 252.20 kB +0.30% 46.15 kB 46.29 kB
facebook-www/ReactDOMServer-prod.modern.js +0.23% 252.94 kB 253.53 kB +0.24% 45.23 kB 45.34 kB
facebook-www/ReactDOMServer-prod.classic.js +0.23% 255.27 kB 255.87 kB +0.23% 45.58 kB 45.68 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.development.js +0.23% 384.23 kB 385.12 kB +0.25% 73.57 kB 73.75 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.development.js +0.23% 411.47 kB 412.42 kB +0.30% 74.05 kB 74.27 kB
oss-stable/react-dom/cjs/react-dom-server.browser.development.js +0.23% 411.54 kB 412.49 kB +0.30% 74.10 kB 74.32 kB
oss-stable-semver/react-dom/cjs/react-dom-server.edge.development.js +0.23% 412.25 kB 413.20 kB +0.32% 74.23 kB 74.47 kB
oss-stable/react-dom/cjs/react-dom-server.edge.development.js +0.23% 412.32 kB 413.27 kB +0.31% 74.29 kB 74.52 kB
oss-stable-semver/react-dom/cjs/react-dom-server.node.development.js +0.23% 412.43 kB 413.37 kB +0.28% 72.96 kB 73.16 kB
oss-stable/react-dom/cjs/react-dom-server.node.development.js +0.23% 412.51 kB 413.45 kB +0.28% 73.01 kB 73.22 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.production.js +0.23% 264.24 kB 264.84 kB +0.24% 46.89 kB 47.00 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.production.js +0.22% 269.78 kB 270.38 kB +0.21% 49.02 kB 49.13 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.development.js +0.22% 455.52 kB 456.52 kB +0.24% 79.79 kB 79.98 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.development.js +0.22% 456.53 kB 457.53 kB +0.24% 80.02 kB 80.21 kB
oss-stable-semver/react-server/cjs/react-server.development.js +0.22% 197.18 kB 197.61 kB +0.23% 34.86 kB 34.95 kB
oss-stable/react-server/cjs/react-server.development.js +0.22% 197.18 kB 197.61 kB +0.23% 34.86 kB 34.95 kB
oss-experimental/react-dom/cjs/react-dom-server.node.development.js +0.21% 462.51 kB 463.51 kB +0.27% 79.77 kB 79.99 kB
oss-experimental/react-server/cjs/react-server.development.js +0.20% 215.95 kB 216.38 kB +0.21% 37.38 kB 37.46 kB

Generated by 🚫 dangerJS against a9f2324

<head>
<link rel="stylesheet" href="A" data-precedence="A" />
<link rel="stylesheet" href="AA" data-precedence="AA" />
<link rel="stylesheet" href="A" data-precedence="A" />
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The change in precedence here is concerning. It suggests an existing bug that if you suspend or not in a sibling (which causes outlining) can change the precedence.

Shouldn't these always be registered by when we first observe the precedence?

Copy link
Collaborator

Choose a reason for hiding this comment

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

They are but we don't flush all preceding precedences just to unblock a particular boundary. The way things worked before we should in theory always hoist stylesheets of an inner boundary to the outer if the outer hadn't revealed yet. For some reason that's not happening here and so the later precedences end up appearing earlier in the document. I'll have to look at it more because I can't yet understand why outlining would lead to this since you didn't change the part of the code that does the hoisting. But perhaps the outlining branch needs to explicitly hoist and it doesn't and now this test hits that path instead of the complete boundary path so it ends up skipping a hoist that it should not have.

Oh it's probably because the parent says it flushed and we're relying on the parent flushed to decide whether to hoist or not.

Copy link
Collaborator

Choose a reason for hiding this comment

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

re

Shouldn't these always be registered by when we first observe the precedence

we could emit dummy style tags with precedence to fill out the precedence order anytime we are about to flush a precedence that has other precedences that precede it. I didn't do this at first b/c I didn't think it was necessary but it seems like it is.

Copy link
Collaborator Author

@sebmarkbage sebmarkbage Sep 24, 2025

Choose a reason for hiding this comment

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

I solved the immediate problem here by skipping outlining while were emitting partial boundaries. I.e. when a parent blocks a child.

When it comes to pure bytes it doesn't make sense to outline in a partial boundary since the resolution of the parent will come later.

When it comes to outlining because of client resources, it can still make sense to outline in case the parent resolved before the CSS or the image in the child has loaded. However, there's always a tradeoff to outlining so in this case maybe it's ok to skip it.

The consequence is that the parent can be blocked for longer if the CSS gets hoisted, or in the case that the image is blocking then the parent's animation can be blocked on the image. Delaying the parent. However, if the parent doesn't have an animation then it's instead possible that the image isn't blocking but that's only if you have a separate boundary with no animation above which is not resolved initially. Which is not really a case this PR is trying to optimize for. It's mainly when there's a full shell above and you're animating in from that.

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 do believe that there's a separate existing bug unrelated to this PR if a child boundary is also suspended but unsuspends before the parent unsuspends. That would trigger this same scenario. Since ultimately it's because the "complete" boundary instruction of the child would insert it while it's actually still in a hidden subtree.

It could either track all pending parent suspense boundaries to know if it's still in a hidden tree on the server but that suffers from the problem of hoisting something up and now blocking the parent. Alternatively it could track this on the client that a complete only inserts its CSS after the parent is complete.

…e size is not exceeded

In this case stylesheets.
…ch might animate

Since the client runtime doesn't implement suspensey image semantics unless we're animating, there's
no point in outlining unless it's inside a Suspense boundary with an enter animation or a <ViewTransition> around
the boundary.
We don't outline when we're emitting partially completed boundaries optimistically
because it doesn't make sense to outline something if its parent is going to be
blocked on something later in the stream anyway. We won't be able to show
the parent anyway so we might as well inline.

This is not completely true for when we outline for CSS or images because
the reveal of the inner boundary could be still pending after the parent
finishes but there's a trade off there.
@sebmarkbage sebmarkbage force-pushed the fizzoutlinesuspenseyimage branch from fd523ca to a9f2324 Compare September 24, 2025 20:07
@sebmarkbage sebmarkbage merged commit 6eb5d67 into facebook:main Sep 25, 2025
241 checks passed
github-actions bot pushed a commit that referenced this pull request Sep 25, 2025
…34552)

We should favor outlining a boundary if it contains Suspensey CSS or
Suspensey Images since then we can load that content separately and not
block the main content. This also allows us to animate the reveal.

For example this should be able to animate the reveal even though the
actual HTML content isn't large in this case it's worth outlining so
that the JS runtime can choose to animate this reveal.

```js
<ViewTransition>
  <Suspense>
    <img src="..." />
  </Suspense>
</ViewTransition>
```

For Suspensey Images, in Fizz, we currently only implement the suspensey
semantics when a View Transition is running. Therefore the outlining
only applies if it appears inside a Suspense boundary which might
animate. Otherwise there's no point in outlining. It is also only if the
Suspense boundary itself might animate its appear and not just any
ViewTransition. So the effect is very conservative.

For CSS it applies even without ViewTransition though, since it can help
unblock the main content faster.

DiffTrain build for [6eb5d67](6eb5d67)
EugeneChoi4 pushed a commit to EugeneChoi4/react that referenced this pull request Sep 29, 2025
…acebook#34552)

We should favor outlining a boundary if it contains Suspensey CSS or
Suspensey Images since then we can load that content separately and not
block the main content. This also allows us to animate the reveal.

For example this should be able to animate the reveal even though the
actual HTML content isn't large in this case it's worth outlining so
that the JS runtime can choose to animate this reveal.

```js
<ViewTransition>
  <Suspense>
    <img src="..." />
  </Suspense>
</ViewTransition>
```

For Suspensey Images, in Fizz, we currently only implement the suspensey
semantics when a View Transition is running. Therefore the outlining
only applies if it appears inside a Suspense boundary which might
animate. Otherwise there's no point in outlining. It is also only if the
Suspense boundary itself might animate its appear and not just any
ViewTransition. So the effect is very conservative.

For CSS it applies even without ViewTransition though, since it can help
unblock the main content faster.
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.

3 participants