Skip to content
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

[Fizz] Support Suspense boundaries anywhere #32069

Merged
merged 1 commit into from
Jan 17, 2025

Conversation

gnoff
Copy link
Collaborator

@gnoff gnoff commented Jan 14, 2025

Suspense is meant to be composable but there has been a lonstanding limitation with using Suspense above the <body> tag of an HTML document due to peculiarities of how HTML is parsed. For instance if you used Suspense to render an entire HTML document and had a fallback that might flush an alternate Document the comment nodes which describe this boundary scope won't be where they need to be in the DOM for client React to properly hydrate them. This is somewhat a problem of our own making in that we have a concept of a Preamble and we leave the closing body and html tags behind until streaming has completed which produces a valid HTML document that also matches the DOM structure that would be parsed from it. However Preambles as a concept are too important to features like Float to imagine moving away from this model and so we can either choose to just accept that you cannot use Suspense anywhere except inside the <body> or we can build special support for Suspense into react-dom that has a coherent semantic with how HTML documents are written and parsed.

This change implements Suspense support for react-dom/server by correctly serializing boundaries during rendering, prerendering, and resumgin on the server. It does not yet support Suspense everywhere on the client but this will arrive in a subsequent change. In practice Suspense cannot be used above the <body> tag today so this is not a breaking change since no programs in the wild could be using this feature anyway.

React's streaming rendering of HTML doesn't lend itself to replacing the contents of the documentElement, head, or body of a Document. These are already special cased in fiber as HostSingletons and similarly for Fizz the values we render for these tags must never be updated by the Fizz runtime once written. To accomplish these we redefine the Preamble as the tags that represent these three singletons plus the contents of the document.head. If you use Suspense above any part of the Preamble then nothing will be written to the destination until the boundary is no longer pending. If the boundary completes then the preamble from within that boudnary will be output. If the boundary postpones or errors then the preamble from the fallback will be used instead.

Additionally, by default anything that is not part of the preamble is implicitly in body scope. This leads to the somewhat counterintuitive consequence that the comment nodes we use to mark the borders of a Suspense boundary in Fizz can appear INSIDE the preamble that was rendered within it.

render((
  <Suspense>
    <html lang="en">
      <body>
        <div>hello world</div>
      </body>
    </html>
  </Suspense>
))

will produce an HTML document like this

<!DOCTYPE html>
<html lang="en">
  <head></head>
  <body>
    <!--$--> <-- this is the comment Node representing the outermost Suspense
    <div>hello world</div>
    <$--/$-->
  </body>
</html>

Later when I update Fiber to support Suspense anywhere hydration will similarly start implicitly in the document body when the root is part of the preamble (the document or one of it's singletons).

@react-sizebot
Copy link

react-sizebot commented Jan 17, 2025

Comparing: 77656c5...673cfa7

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 = 514.24 kB 514.24 kB = 91.74 kB 91.74 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 = 556.18 kB 556.18 kB = 98.72 kB 98.72 kB
facebook-www/ReactDOM-prod.classic.js = 595.79 kB 595.79 kB = 104.85 kB 104.85 kB
facebook-www/ReactDOM-prod.modern.js = 586.21 kB 586.21 kB = 103.30 kB 103.30 kB
oss-stable-semver/react-server/cjs/react-server.development.js +5.99% 156.94 kB 166.34 kB +4.88% 28.57 kB 29.97 kB
oss-stable/react-server/cjs/react-server.development.js +5.99% 156.94 kB 166.34 kB +4.88% 28.57 kB 29.97 kB
oss-stable-semver/react-server/cjs/react-server.production.js +5.83% 108.97 kB 115.33 kB +4.39% 19.75 kB 20.62 kB
oss-stable/react-server/cjs/react-server.production.js +5.83% 108.97 kB 115.33 kB +4.39% 19.75 kB 20.62 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-server.development.js +5.53% 6.92 kB 7.31 kB +3.17% 1.54 kB 1.59 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-server.development.js +5.53% 6.92 kB 7.31 kB +3.17% 1.54 kB 1.59 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-server.development.js +5.53% 6.92 kB 7.31 kB +3.17% 1.54 kB 1.59 kB
oss-experimental/react-server/cjs/react-server.development.js +5.22% 178.01 kB 187.30 kB +4.23% 31.38 kB 32.71 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-server.production.js +5.20% 5.75 kB 6.05 kB +3.20% 1.47 kB 1.52 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-server.production.js +5.20% 5.75 kB 6.05 kB +3.20% 1.47 kB 1.52 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-server.production.js +5.20% 5.75 kB 6.05 kB +3.20% 1.47 kB 1.52 kB
oss-experimental/react-server/cjs/react-server.production.js +5.19% 122.72 kB 129.09 kB +4.04% 21.58 kB 22.45 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.browser.production.js +3.67% 202.00 kB 209.41 kB +2.82% 37.18 kB 38.23 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.browser.production.js +3.67% 202.02 kB 209.44 kB +2.82% 37.21 kB 38.26 kB
oss-experimental/react-markup/cjs/react-markup.production.js +3.65% 207.66 kB 215.24 kB +2.85% 38.64 kB 39.74 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.node.production.js +3.65% 206.40 kB 213.93 kB +2.79% 38.92 kB 40.00 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.node.production.js +3.65% 206.43 kB 213.95 kB +2.77% 38.94 kB 40.02 kB
facebook-www/ReactDOMServer-prod.modern.js +3.62% 209.48 kB 217.07 kB +2.75% 38.25 kB 39.30 kB
oss-stable-semver/react-dom/cjs/react-dom-server.bun.production.js +3.61% 206.26 kB 213.71 kB +2.85% 38.36 kB 39.45 kB
oss-stable/react-dom/cjs/react-dom-server.bun.production.js +3.61% 206.34 kB 213.79 kB +2.84% 38.39 kB 39.48 kB
facebook-www/ReactDOMServerStreaming-prod.modern.js +3.59% 213.71 kB 221.38 kB +2.91% 39.77 kB 40.93 kB
facebook-www/ReactDOMServer-prod.classic.js +3.56% 212.21 kB 219.75 kB +2.68% 38.60 kB 39.63 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.production.js +3.48% 219.33 kB 226.97 kB +2.68% 39.44 kB 40.49 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.production.js +3.46% 224.30 kB 232.05 kB +2.70% 41.29 kB 42.41 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.production.js +3.37% 225.27 kB 232.87 kB +2.68% 40.76 kB 41.86 kB
oss-stable-semver/react-dom/cjs/react-dom-server.node.production.js +3.34% 222.41 kB 229.85 kB +2.68% 40.79 kB 41.89 kB
oss-stable/react-dom/cjs/react-dom-server.node.production.js +3.34% 222.48 kB 229.92 kB +2.67% 40.82 kB 41.91 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.production.js +3.32% 220.77 kB 228.10 kB +2.70% 39.83 kB 40.90 kB
oss-stable/react-dom/cjs/react-dom-server.browser.production.js +3.32% 220.85 kB 228.17 kB +2.69% 39.85 kB 40.93 kB
oss-stable-semver/react-dom/cjs/react-dom-server.edge.production.js +3.29% 225.86 kB 233.30 kB +2.61% 41.77 kB 42.86 kB
oss-stable/react-dom/cjs/react-dom-server.edge.production.js +3.29% 225.94 kB 233.38 kB +2.60% 41.80 kB 42.89 kB
oss-experimental/react-dom/cjs/react-dom-server.node.production.js +3.15% 248.28 kB 256.09 kB +2.50% 44.01 kB 45.11 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.production.js +3.12% 246.57 kB 254.27 kB +2.52% 42.88 kB 43.96 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.production.js +3.10% 252.23 kB 260.04 kB +2.44% 44.93 kB 46.03 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.node.development.js +2.52% 346.51 kB 355.25 kB +1.92% 63.44 kB 64.66 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.browser.development.js +2.52% 346.52 kB 355.25 kB +1.92% 63.44 kB 64.66 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.node.development.js +2.52% 346.54 kB 355.27 kB +1.91% 63.47 kB 64.68 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.browser.development.js +2.52% 346.54 kB 355.28 kB +1.91% 63.47 kB 64.68 kB
oss-stable-semver/react-dom/cjs/react-dom-server.bun.development.js +2.51% 308.77 kB 316.53 kB +2.02% 60.39 kB 61.61 kB
oss-stable/react-dom/cjs/react-dom-server.bun.development.js +2.51% 308.84 kB 316.60 kB +2.02% 60.42 kB 61.63 kB
oss-experimental/react-markup/cjs/react-markup.development.js +2.49% 347.62 kB 356.28 kB +1.85% 62.80 kB 63.96 kB
facebook-www/ReactDOMServerStreaming-dev.modern.js +2.48% 354.59 kB 363.39 kB +1.87% 64.48 kB 65.68 kB
facebook-www/ReactDOMServer-dev.modern.js +2.48% 362.85 kB 371.83 kB +1.74% 65.79 kB 66.94 kB
oss-experimental/react-markup/cjs/react-markup.react-server.production.js +2.47% 307.39 kB 314.97 kB +1.98% 57.74 kB 58.89 kB
oss-stable-semver/react-dom/cjs/react-dom-server.node.development.js +2.45% 358.46 kB 367.24 kB +1.87% 64.97 kB 66.19 kB
oss-stable/react-dom/cjs/react-dom-server.node.development.js +2.45% 358.54 kB 367.31 kB +1.86% 65.03 kB 66.24 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.development.js +2.42% 361.92 kB 370.69 kB +1.82% 65.64 kB 66.83 kB
oss-stable/react-dom/cjs/react-dom-server.browser.development.js +2.42% 361.99 kB 370.77 kB +1.81% 65.69 kB 66.88 kB
oss-stable-semver/react-dom/cjs/react-dom-server.edge.development.js +2.42% 362.69 kB 371.46 kB +1.82% 65.80 kB 66.99 kB
oss-stable/react-dom/cjs/react-dom-server.edge.development.js +2.42% 362.76 kB 371.54 kB +1.81% 65.85 kB 67.05 kB
facebook-www/ReactDOMServer-dev.classic.js +2.42% 369.72 kB 378.66 kB +1.79% 66.75 kB 67.95 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.development.js +2.34% 375.92 kB 384.72 kB +1.83% 67.01 kB 68.24 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.development.js +2.34% 375.92 kB 384.72 kB +1.83% 67.01 kB 68.24 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.development.js +2.31% 335.83 kB 343.60 kB +1.88% 64.01 kB 65.21 kB
oss-experimental/react-dom/cjs/react-dom-server.node.development.js +2.27% 396.51 kB 405.52 kB +1.81% 69.30 kB 70.56 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.development.js +2.25% 400.56 kB 409.57 kB +1.79% 69.86 kB 71.11 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.development.js +2.24% 401.34 kB 410.35 kB +1.77% 70.02 kB 71.26 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.development.js +5.99% 156.94 kB 166.34 kB +4.88% 28.57 kB 29.97 kB
oss-stable/react-server/cjs/react-server.development.js +5.99% 156.94 kB 166.34 kB +4.88% 28.57 kB 29.97 kB
oss-stable-semver/react-server/cjs/react-server.production.js +5.83% 108.97 kB 115.33 kB +4.39% 19.75 kB 20.62 kB
oss-stable/react-server/cjs/react-server.production.js +5.83% 108.97 kB 115.33 kB +4.39% 19.75 kB 20.62 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-server.development.js +5.53% 6.92 kB 7.31 kB +3.17% 1.54 kB 1.59 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-server.development.js +5.53% 6.92 kB 7.31 kB +3.17% 1.54 kB 1.59 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-server.development.js +5.53% 6.92 kB 7.31 kB +3.17% 1.54 kB 1.59 kB
oss-experimental/react-server/cjs/react-server.development.js +5.22% 178.01 kB 187.30 kB +4.23% 31.38 kB 32.71 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-server.production.js +5.20% 5.75 kB 6.05 kB +3.20% 1.47 kB 1.52 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-server.production.js +5.20% 5.75 kB 6.05 kB +3.20% 1.47 kB 1.52 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-server.production.js +5.20% 5.75 kB 6.05 kB +3.20% 1.47 kB 1.52 kB
oss-experimental/react-server/cjs/react-server.production.js +5.19% 122.72 kB 129.09 kB +4.04% 21.58 kB 22.45 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.browser.production.js +3.67% 202.00 kB 209.41 kB +2.82% 37.18 kB 38.23 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.browser.production.js +3.67% 202.02 kB 209.44 kB +2.82% 37.21 kB 38.26 kB
oss-experimental/react-markup/cjs/react-markup.production.js +3.65% 207.66 kB 215.24 kB +2.85% 38.64 kB 39.74 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.node.production.js +3.65% 206.40 kB 213.93 kB +2.79% 38.92 kB 40.00 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.node.production.js +3.65% 206.43 kB 213.95 kB +2.77% 38.94 kB 40.02 kB
facebook-www/ReactDOMServer-prod.modern.js +3.62% 209.48 kB 217.07 kB +2.75% 38.25 kB 39.30 kB
oss-stable-semver/react-dom/cjs/react-dom-server.bun.production.js +3.61% 206.26 kB 213.71 kB +2.85% 38.36 kB 39.45 kB
oss-stable/react-dom/cjs/react-dom-server.bun.production.js +3.61% 206.34 kB 213.79 kB +2.84% 38.39 kB 39.48 kB
facebook-www/ReactDOMServerStreaming-prod.modern.js +3.59% 213.71 kB 221.38 kB +2.91% 39.77 kB 40.93 kB
facebook-www/ReactDOMServer-prod.classic.js +3.56% 212.21 kB 219.75 kB +2.68% 38.60 kB 39.63 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.production.js +3.48% 219.33 kB 226.97 kB +2.68% 39.44 kB 40.49 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.production.js +3.46% 224.30 kB 232.05 kB +2.70% 41.29 kB 42.41 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.production.js +3.37% 225.27 kB 232.87 kB +2.68% 40.76 kB 41.86 kB
oss-stable-semver/react-dom/cjs/react-dom-server.node.production.js +3.34% 222.41 kB 229.85 kB +2.68% 40.79 kB 41.89 kB
oss-stable/react-dom/cjs/react-dom-server.node.production.js +3.34% 222.48 kB 229.92 kB +2.67% 40.82 kB 41.91 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.production.js +3.32% 220.77 kB 228.10 kB +2.70% 39.83 kB 40.90 kB
oss-stable/react-dom/cjs/react-dom-server.browser.production.js +3.32% 220.85 kB 228.17 kB +2.69% 39.85 kB 40.93 kB
oss-stable-semver/react-dom/cjs/react-dom-server.edge.production.js +3.29% 225.86 kB 233.30 kB +2.61% 41.77 kB 42.86 kB
oss-stable/react-dom/cjs/react-dom-server.edge.production.js +3.29% 225.94 kB 233.38 kB +2.60% 41.80 kB 42.89 kB
oss-experimental/react-dom/cjs/react-dom-server.node.production.js +3.15% 248.28 kB 256.09 kB +2.50% 44.01 kB 45.11 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.production.js +3.12% 246.57 kB 254.27 kB +2.52% 42.88 kB 43.96 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.production.js +3.10% 252.23 kB 260.04 kB +2.44% 44.93 kB 46.03 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.node.development.js +2.52% 346.51 kB 355.25 kB +1.92% 63.44 kB 64.66 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.browser.development.js +2.52% 346.52 kB 355.25 kB +1.92% 63.44 kB 64.66 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.node.development.js +2.52% 346.54 kB 355.27 kB +1.91% 63.47 kB 64.68 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.browser.development.js +2.52% 346.54 kB 355.28 kB +1.91% 63.47 kB 64.68 kB
oss-stable-semver/react-dom/cjs/react-dom-server.bun.development.js +2.51% 308.77 kB 316.53 kB +2.02% 60.39 kB 61.61 kB
oss-stable/react-dom/cjs/react-dom-server.bun.development.js +2.51% 308.84 kB 316.60 kB +2.02% 60.42 kB 61.63 kB
oss-experimental/react-markup/cjs/react-markup.development.js +2.49% 347.62 kB 356.28 kB +1.85% 62.80 kB 63.96 kB
facebook-www/ReactDOMServerStreaming-dev.modern.js +2.48% 354.59 kB 363.39 kB +1.87% 64.48 kB 65.68 kB
facebook-www/ReactDOMServer-dev.modern.js +2.48% 362.85 kB 371.83 kB +1.74% 65.79 kB 66.94 kB
oss-experimental/react-markup/cjs/react-markup.react-server.production.js +2.47% 307.39 kB 314.97 kB +1.98% 57.74 kB 58.89 kB
oss-stable-semver/react-dom/cjs/react-dom-server.node.development.js +2.45% 358.46 kB 367.24 kB +1.87% 64.97 kB 66.19 kB
oss-stable/react-dom/cjs/react-dom-server.node.development.js +2.45% 358.54 kB 367.31 kB +1.86% 65.03 kB 66.24 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.development.js +2.42% 361.92 kB 370.69 kB +1.82% 65.64 kB 66.83 kB
oss-stable/react-dom/cjs/react-dom-server.browser.development.js +2.42% 361.99 kB 370.77 kB +1.81% 65.69 kB 66.88 kB
oss-stable-semver/react-dom/cjs/react-dom-server.edge.development.js +2.42% 362.69 kB 371.46 kB +1.82% 65.80 kB 66.99 kB
oss-stable/react-dom/cjs/react-dom-server.edge.development.js +2.42% 362.76 kB 371.54 kB +1.81% 65.85 kB 67.05 kB
facebook-www/ReactDOMServer-dev.classic.js +2.42% 369.72 kB 378.66 kB +1.79% 66.75 kB 67.95 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.development.js +2.34% 375.92 kB 384.72 kB +1.83% 67.01 kB 68.24 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.development.js +2.34% 375.92 kB 384.72 kB +1.83% 67.01 kB 68.24 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.development.js +2.31% 335.83 kB 343.60 kB +1.88% 64.01 kB 65.21 kB
oss-experimental/react-dom/cjs/react-dom-server.node.development.js +2.27% 396.51 kB 405.52 kB +1.81% 69.30 kB 70.56 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.development.js +2.25% 400.56 kB 409.57 kB +1.79% 69.86 kB 71.11 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.development.js +2.24% 401.34 kB 410.35 kB +1.77% 70.02 kB 71.26 kB
oss-experimental/react-markup/cjs/react-markup.react-server.development.js +1.66% 520.55 kB 529.22 kB +1.24% 93.72 kB 94.88 kB

Generated by 🚫 dangerJS against bdd0b24

@@ -13,7 +13,7 @@
// Use __VARIANT__ to simulate a GK. The tests will be run twice: once
// with the __VARIANT__ set to `true`, and once set to `false`.

export const alwaysThrottleRetries = true;
export const alwaysThrottleRetries = __VARIANT__;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this relevant?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no I mistakenly reset this while debugging a different variant bug failing a test that was actually a problem in the test itself

if (
request.pendingRootTasks === 0 &&
request.trackedPostpones === null &&
boundary.contentPreamble
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit. boundary.contentPreamble !== null. Helps catch bugs when the types are not expected.

if (
request.pendingRootTasks === 0 &&
request.trackedPostpones === null &&
boundary.contentPreamble
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same.

Suspense is meant to be composable but there has been a lonstanding limitation with using Suspense above the `<body>` tag of an HTML document due to peculiarities of how HTML is parsed. For instance if you used Suspense to render an entire HTML document and had a fallback that might flush an alternate Document the comment nodes which describe this boundary scope won't be where they need to be in the DOM for client React to properly hydrate them. This is somewhat a problem of our own making in that we have a concept of a Preamble and we leave the closing body and html tags behind until streaming has completed which produces a valid HTML document that also matches the DOM structure that would be parsed from it. However Preambles as a concept are too important to features like Float to imagine moving away from this model and so we can either choose to just accept that you cannot use Suspense anywhere except inside the `<body>` or we can build special support for Suspense into react-dom that has a coherent semantic with how HTML documents are written and parsed.

This change implements Suspense support for react-dom/server by correctly serializing boundaries during rendering, prerendering, and resumgin on the server. It does not yet support Suspense everywhere on the client but this will arrive in a subsequent change. In practice Suspense cannot be used above the <body> tag today so this is not a breaking change since no programs in the wild could be using this feature anyway.

React's streaming rendering of HTML doesn't lend itself to replacing the contents of the documentElement, head, or body of a Document. These are already special cased in fiber as HostSingletons and similarly for Fizz the values we render for these tags must never be updated by the Fizz runtime once written. To accomplish these we redefine the Preamble as the tags that represent these three singletons plus the contents of the document.head. If you use Suspense above any part of the Preamble then nothing will be written to the destination until the boundary is no longer pending. If the boundary completes then the preamble from within that boudnary will be output. If the boundary postpones or errors then the preamble from the fallback will be used instead.

Additionally, by default anything that is not part of the preamble is implicitly in body scope. This leads to the somewhat counterintuitive consequence that the comment nodes we use to mark the borders of a Suspense boundary in Fizz can appear INSIDE the preamble that was rendered within it.

Later when I update Fiber to support Suspense anywhere hydration will similarly start implicitly in the document body when the root is part of the preamble (the document or one of it's singletons).
@gnoff gnoff force-pushed the suspense-anywhere-ssr branch from cb7612f to bdd0b24 Compare January 17, 2025 18:18
@gnoff gnoff merged commit b25bcd4 into facebook:main Jan 17, 2025
188 checks passed
@gnoff gnoff deleted the suspense-anywhere-ssr branch January 17, 2025 18:54
github-actions bot pushed a commit that referenced this pull request Jan 17, 2025
Suspense is meant to be composable but there has been a lonstanding
limitation with using Suspense above the `<body>` tag of an HTML
document due to peculiarities of how HTML is parsed. For instance if you
used Suspense to render an entire HTML document and had a fallback that
might flush an alternate Document the comment nodes which describe this
boundary scope won't be where they need to be in the DOM for client
React to properly hydrate them. This is somewhat a problem of our own
making in that we have a concept of a Preamble and we leave the closing
body and html tags behind until streaming has completed which produces a
valid HTML document that also matches the DOM structure that would be
parsed from it. However Preambles as a concept are too important to
features like Float to imagine moving away from this model and so we can
either choose to just accept that you cannot use Suspense anywhere
except inside the `<body>` or we can build special support for Suspense
into react-dom that has a coherent semantic with how HTML documents are
written and parsed.

This change implements Suspense support for react-dom/server by
correctly serializing boundaries during rendering, prerendering, and
resumgin on the server. It does not yet support Suspense everywhere on
the client but this will arrive in a subsequent change. In practice
Suspense cannot be used above the `<body>` tag today so this is not a
breaking change since no programs in the wild could be using this
feature anyway.

React's streaming rendering of HTML doesn't lend itself to replacing the
contents of the documentElement, head, or body of a Document. These are
already special cased in fiber as HostSingletons and similarly for Fizz
the values we render for these tags must never be updated by the Fizz
runtime once written. To accomplish these we redefine the Preamble as
the tags that represent these three singletons plus the contents of the
document.head. If you use Suspense above any part of the Preamble then
nothing will be written to the destination until the boundary is no
longer pending. If the boundary completes then the preamble from within
that boudnary will be output. If the boundary postpones or errors then
the preamble from the fallback will be used instead.

Additionally, by default anything that is not part of the preamble is
implicitly in body scope. This leads to the somewhat counterintuitive
consequence that the comment nodes we use to mark the borders of a
Suspense boundary in Fizz can appear INSIDE the preamble that was
rendered within it.

```typescript
render((
  <Suspense>
    <html lang="en">
      <body>
        <div>hello world</div>
      </body>
    </html>
  </Suspense>
))
```
will produce an HTML document like this
```html
<!DOCTYPE html>
<html lang="en">
  <head></head>
  <body>
    <!--$--> <-- this is the comment Node representing the outermost Suspense
    <div>hello world</div>
    <$--/$-->
  </body>
</html>
```

Later when I update Fiber to support Suspense anywhere hydration will
similarly start implicitly in the document body when the root is part of
the preamble (the document or one of it's singletons).

DiffTrain build for [b25bcd4](b25bcd4)
gnoff added a commit to gnoff/react that referenced this pull request Jan 22, 2025
This is a follow up to facebook#32069

In the prior change I updated Fizz to allow you to render Suspense boundaries at any level within a react-dom application by treating the document body as the default render scope. This change updates Fiber to provide similar semantics. Note that this update still does not deliver hydration so unifying the Fizz and Fiber implementations in a single App is not possible yet.

The implementation required a rework of the getHostSibling and getHostParent algorithms. Now most HostSingletons are invisible from a host positioning perspective. Head is special in that it is a valid host scope so when you have Placements inside of it, it will act as the parent. But body, and html, will not directly participate in host positioning.

Additionally to support flipping to a fallback <html>, <head>, and <body> tag in a Suspense fallback I updated the offscreen hiding/unhide logic to pierce through singletons when lookin for matching hidable nod boundaries anywhere (excluding hydration)
gnoff added a commit to gnoff/react that referenced this pull request Jan 22, 2025
This is a follow up to facebook#32069

In the prior change I updated Fizz to allow you to render Suspense boundaries at any level within a react-dom application by treating the document body as the default render scope. This change updates Fiber to provide similar semantics. Note that this update still does not deliver hydration so unifying the Fizz and Fiber implementations in a single App is not possible yet.

The implementation required a rework of the getHostSibling and getHostParent algorithms. Now most HostSingletons are invisible from a host positioning perspective. Head is special in that it is a valid host scope so when you have Placements inside of it, it will act as the parent. But body, and html, will not directly participate in host positioning.

Additionally to support flipping to a fallback <html>, <head>, and <body> tag in a Suspense fallback I updated the offscreen hiding/unhide logic to pierce through singletons when lookin for matching hidable nod boundaries anywhere (excluding hydration)
gnoff added a commit to gnoff/react that referenced this pull request Jan 22, 2025
This is a follow up to facebook#32069

In the prior change I updated Fizz to allow you to render Suspense boundaries at any level within a react-dom application by treating the document body as the default render scope. This change updates Fiber to provide similar semantics. Note that this update still does not deliver hydration so unifying the Fizz and Fiber implementations in a single App is not possible yet.

The implementation required a rework of the getHostSibling and getHostParent algorithms. Now most HostSingletons are invisible from a host positioning perspective. Head is special in that it is a valid host scope so when you have Placements inside of it, it will act as the parent. But body, and html, will not directly participate in host positioning.

Additionally to support flipping to a fallback <html>, <head>, and <body> tag in a Suspense fallback I updated the offscreen hiding/unhide logic to pierce through singletons when lookin for matching hidable nod boundaries anywhere (excluding hydration)
gnoff added a commit to gnoff/react that referenced this pull request Jan 22, 2025
This is a follow up to facebook#32069

In the prior change I updated Fizz to allow you to render Suspense boundaries at any level within a react-dom application by treating the document body as the default render scope. This change updates Fiber to provide similar semantics. Note that this update still does not deliver hydration so unifying the Fizz and Fiber implementations in a single App is not possible yet.

The implementation required a rework of the getHostSibling and getHostParent algorithms. Now most HostSingletons are invisible from a host positioning perspective. Head is special in that it is a valid host scope so when you have Placements inside of it, it will act as the parent. But body, and html, will not directly participate in host positioning.

Additionally to support flipping to a fallback <html>, <head>, and <body> tag in a Suspense fallback I updated the offscreen hiding/unhide logic to pierce through singletons when lookin for matching hidable nod boundaries anywhere (excluding hydration)
gnoff added a commit to gnoff/react that referenced this pull request Jan 24, 2025
This is a follow up to facebook#32069

In the prior change I updated Fizz to allow you to render Suspense boundaries at any level within a react-dom application by treating the document body as the default render scope. This change updates Fiber to provide similar semantics. Note that this update still does not deliver hydration so unifying the Fizz and Fiber implementations in a single App is not possible yet.

The implementation required a rework of the getHostSibling and getHostParent algorithms. Now most HostSingletons are invisible from a host positioning perspective. Head is special in that it is a valid host scope so when you have Placements inside of it, it will act as the parent. But body, and html, will not directly participate in host positioning.

Additionally to support flipping to a fallback <html>, <head>, and <body> tag in a Suspense fallback I updated the offscreen hiding/unhide logic to pierce through singletons when lookin for matching hidable nod boundaries anywhere (excluding hydration)
gnoff added a commit to gnoff/react that referenced this pull request Jan 24, 2025
This is a follow up to facebook#32069

In the prior change I updated Fizz to allow you to render Suspense boundaries at any level within a react-dom application by treating the document body as the default render scope. This change updates Fiber to provide similar semantics. Note that this update still does not deliver hydration so unifying the Fizz and Fiber implementations in a single App is not possible yet.

The implementation required a rework of the getHostSibling and getHostParent algorithms. Now most HostSingletons are invisible from a host positioning perspective. Head is special in that it is a valid host scope so when you have Placements inside of it, it will act as the parent. But body, and html, will not directly participate in host positioning.

Additionally to support flipping to a fallback <html>, <head>, and <body> tag in a Suspense fallback I updated the offscreen hiding/unhide logic to pierce through singletons when lookin for matching hidable nod boundaries anywhere (excluding hydration)
gnoff added a commit to gnoff/react that referenced this pull request Jan 28, 2025
This is a follow up to facebook#32069

In the prior change I updated Fizz to allow you to render Suspense boundaries at any level within a react-dom application by treating the document body as the default render scope. This change updates Fiber to provide similar semantics. Note that this update still does not deliver hydration so unifying the Fizz and Fiber implementations in a single App is not possible yet.

The implementation required a rework of the getHostSibling and getHostParent algorithms. Now most HostSingletons are invisible from a host positioning perspective. Head is special in that it is a valid host scope so when you have Placements inside of it, it will act as the parent. But body, and html, will not directly participate in host positioning.

Additionally to support flipping to a fallback <html>, <head>, and <body> tag in a Suspense fallback I updated the offscreen hiding/unhide logic to pierce through singletons when lookin for matching hidable nod boundaries anywhere (excluding hydration)
gnoff added a commit that referenced this pull request Jan 29, 2025
…32163)

This is a follow up to #32069

In the prior change I updated Fizz to allow you to render Suspense
boundaries at any level within a react-dom application by treating the
document body as the default render scope. This change updates Fiber to
provide similar semantics. Note that this update still does not deliver
hydration so unifying the Fizz and Fiber implementations in a single App
is not possible yet.

The implementation required a rework of the getHostSibling and
getHostParent algorithms. Now most HostSingletons are invisible from a
host positioning perspective. Head is special in that it is a valid host
scope so when you have Placements inside of it, it will act as the
parent. But body, and html, will not directly participate in host
positioning.

Additionally to support flipping to a fallback html, head, and body tag
in a Suspense fallback I updated the offscreen hiding/unhide logic to
pierce through singletons when lookin for matching hidable nod
boundaries anywhere (excluding hydration)
github-actions bot pushed a commit that referenced this pull request Jan 29, 2025
…32163)

This is a follow up to #32069

In the prior change I updated Fizz to allow you to render Suspense
boundaries at any level within a react-dom application by treating the
document body as the default render scope. This change updates Fiber to
provide similar semantics. Note that this update still does not deliver
hydration so unifying the Fizz and Fiber implementations in a single App
is not possible yet.

The implementation required a rework of the getHostSibling and
getHostParent algorithms. Now most HostSingletons are invisible from a
host positioning perspective. Head is special in that it is a valid host
scope so when you have Placements inside of it, it will act as the
parent. But body, and html, will not directly participate in host
positioning.

Additionally to support flipping to a fallback html, head, and body tag
in a Suspense fallback I updated the offscreen hiding/unhide logic to
pierce through singletons when lookin for matching hidable nod
boundaries anywhere (excluding hydration)

DiffTrain build for [c492f97](c492f97)
github-actions bot pushed a commit that referenced this pull request Jan 29, 2025
…32163)

This is a follow up to #32069

In the prior change I updated Fizz to allow you to render Suspense
boundaries at any level within a react-dom application by treating the
document body as the default render scope. This change updates Fiber to
provide similar semantics. Note that this update still does not deliver
hydration so unifying the Fizz and Fiber implementations in a single App
is not possible yet.

The implementation required a rework of the getHostSibling and
getHostParent algorithms. Now most HostSingletons are invisible from a
host positioning perspective. Head is special in that it is a valid host
scope so when you have Placements inside of it, it will act as the
parent. But body, and html, will not directly participate in host
positioning.

Additionally to support flipping to a fallback html, head, and body tag
in a Suspense fallback I updated the offscreen hiding/unhide logic to
pierce through singletons when lookin for matching hidable nod
boundaries anywhere (excluding hydration)

DiffTrain build for [c492f97](c492f97)
github-actions bot pushed a commit to code/lib-react that referenced this pull request Jan 29, 2025
…acebook#32163)

This is a follow up to facebook#32069

In the prior change I updated Fizz to allow you to render Suspense
boundaries at any level within a react-dom application by treating the
document body as the default render scope. This change updates Fiber to
provide similar semantics. Note that this update still does not deliver
hydration so unifying the Fizz and Fiber implementations in a single App
is not possible yet.

The implementation required a rework of the getHostSibling and
getHostParent algorithms. Now most HostSingletons are invisible from a
host positioning perspective. Head is special in that it is a valid host
scope so when you have Placements inside of it, it will act as the
parent. But body, and html, will not directly participate in host
positioning.

Additionally to support flipping to a fallback html, head, and body tag
in a Suspense fallback I updated the offscreen hiding/unhide logic to
pierce through singletons when lookin for matching hidable nod
boundaries anywhere (excluding hydration)

DiffTrain build for [c492f97](facebook@c492f97)
github-actions bot pushed a commit to code/lib-react that referenced this pull request Jan 29, 2025
…acebook#32163)

This is a follow up to facebook#32069

In the prior change I updated Fizz to allow you to render Suspense
boundaries at any level within a react-dom application by treating the
document body as the default render scope. This change updates Fiber to
provide similar semantics. Note that this update still does not deliver
hydration so unifying the Fizz and Fiber implementations in a single App
is not possible yet.

The implementation required a rework of the getHostSibling and
getHostParent algorithms. Now most HostSingletons are invisible from a
host positioning perspective. Head is special in that it is a valid host
scope so when you have Placements inside of it, it will act as the
parent. But body, and html, will not directly participate in host
positioning.

Additionally to support flipping to a fallback html, head, and body tag
in a Suspense fallback I updated the offscreen hiding/unhide logic to
pierce through singletons when lookin for matching hidable nod
boundaries anywhere (excluding hydration)

DiffTrain build for [c492f97](facebook@c492f97)
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.

4 participants