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

[Flight] Deduplicate suspended elements #28748

Closed

Conversation

unstubbable
Copy link
Contributor

@unstubbable unstubbable commented Apr 4, 2024

This is a follow-up from the finding I've documented in #28620 (comment):

The added unit test also shows another potential issue here: In production mode, the async server component elements are emitted twice:

1:"$Sreact.suspense"
0:["$","$1",null,{"fallback":["$","p",null,{"children":"loading"}],"children":"$L2"}]
2:["$","$1",null,{"fallback":["$","$1",null,{"fallback":["$","p",null,{"children":"loading posts and photos"}],"children":["$L3","$L4"]}],"children":"$L5"}]
9:{"children":"loading posts and photos"}
8:["$","p",null,"$9"]
a:["$b","$c"]
7:{"fallback":"$8","children":"$a"}
6:["$","$1",null,"$7"]
5:["$","div",null,{"children":"$6"}]
3:["$","div",null,{"children":"posts"}]
b:["$","div",null,{"children":"posts"}]
4:["$","div",null,{"children":"photos"}]
c:["$","div",null,{"children":"photos"}]

Whereas in dev mode, due to the outlineTask call for assigning debug info, they are emitted only once:

1:"$Sreact.suspense"
2:D{"name":"Row","env":"Server"}
0:["$","$1",null,{"fallback":["$","p",null,{"children":"loading"}],"children":"$L2"}]
3:D{"name":"DelayedText","env":"Server"}
4:D{"name":"DelayedText","env":"Server"}
5:D{"name":"Row","env":"Server"}
2:["$","$1",null,{"fallback":["$","$1",null,{"fallback":["$","p",null,{"children":"loading posts and photos"}],"children":["$L3","$L4"]}],"children":"$L5"}]
9:{"children":"loading posts and photos"}
8:["$","p",null,"$9"]
a:["$3","$4"]
7:{"fallback":"$8","children":"$a"}
6:["$","$1",null,"$7"]
5:["$","div",null,{"children":"$6"}]
3:["$","div",null,{"children":"posts"}]
4:["$","div",null,{"children":"photos"}]

In #27537 the following statement was made:

If we for some other reason outline an object such as if it suspends, then it's truly deduplicated since it already has an id.

So it seems that the deduplication is indeed supposed to work for such cases (as opposed to references to other elements, where only "detriplication" is applied).

This PR ensures that this works if suspended elements are referenced twice within the same model root. This is usually the case when streaming async server components with ai/rsc, where the element is used in a suspense fallback, and also referenced in its children, see https://github.com/vercel/ai/blob/5e00440e574f78b4129848deb56da8b354d04dc2/packages/core/rsc/utils.tsx#L42-L44.

In addition to the unit tests, I've also tested this end-to-end with https://github.com/unstubbable/mfng-ai-demo.

@react-sizebot
Copy link

react-sizebot commented Apr 4, 2024

Comparing: 46abd7b...718e73a

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.66 kB 6.66 kB +0.05% 1.82 kB 1.82 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 492.61 kB 492.61 kB = 87.88 kB 87.88 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.67 kB 6.67 kB +0.11% 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 498.86 kB 498.86 kB = 88.93 kB 88.93 kB
facebook-www/ReactDOM-prod.classic.js = 591.22 kB 591.22 kB = 103.96 kB 103.96 kB
facebook-www/ReactDOM-prod.modern.js = 567.44 kB 567.44 kB = 100.36 kB 100.36 kB
test_utils/ReactAllWarnings.js Deleted 64.26 kB 0.00 kB Deleted 16.02 kB 0.00 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-dom-esm/cjs/react-server-dom-esm-client.browser.production.js +0.85% 29.03 kB 29.27 kB +1.57% 6.56 kB 6.67 kB
oss-stable/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.production.js +0.85% 29.03 kB 29.27 kB +1.57% 6.56 kB 6.67 kB
oss-stable-semver/react-client/cjs/react-client-flight.production.js +0.81% 30.52 kB 30.77 kB +1.40% 6.51 kB 6.60 kB
oss-stable/react-client/cjs/react-client-flight.production.js +0.81% 30.52 kB 30.77 kB +1.40% 6.51 kB 6.60 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.browser.production.js +0.80% 30.57 kB 30.82 kB +1.53% 6.94 kB 7.05 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.browser.production.js +0.80% 30.57 kB 30.82 kB +1.53% 6.94 kB 7.05 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.production.js +0.80% 30.91 kB 31.15 kB +1.48% 7.02 kB 7.12 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.production.js +0.80% 30.91 kB 31.15 kB +1.48% 7.02 kB 7.12 kB
oss-stable-semver/react-server-dom-esm/cjs/react-server-dom-esm-client.node.production.js +0.74% 33.22 kB 33.46 kB +1.38% 7.56 kB 7.66 kB
oss-stable/react-server-dom-esm/cjs/react-server-dom-esm-client.node.production.js +0.74% 33.22 kB 33.46 kB +1.38% 7.56 kB 7.66 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.unbundled.production.js +0.71% 34.47 kB 34.71 kB +1.34% 7.83 kB 7.93 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.unbundled.production.js +0.71% 34.47 kB 34.71 kB +1.34% 7.83 kB 7.93 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.production.js +0.71% 34.47 kB 34.71 kB +1.30% 7.84 kB 7.94 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.production.js +0.71% 34.47 kB 34.71 kB +1.30% 7.84 kB 7.94 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.production.js +0.70% 35.15 kB 35.39 kB +1.27% 8.01 kB 8.11 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.production.js +0.70% 35.15 kB 35.39 kB +1.27% 8.01 kB 8.11 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.production.js +0.70% 35.16 kB 35.40 kB +1.31% 8.00 kB 8.10 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.production.js +0.70% 35.16 kB 35.40 kB +1.31% 8.00 kB 8.10 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.production.js +0.68% 36.38 kB 36.63 kB +1.18% 8.21 kB 8.31 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.production.js +0.68% 36.38 kB 36.63 kB +1.18% 8.21 kB 8.31 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.edge.production.js +0.68% 36.39 kB 36.64 kB +1.22% 8.20 kB 8.30 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.edge.production.js +0.68% 36.39 kB 36.64 kB +1.22% 8.20 kB 8.30 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.production.js +0.54% 42.72 kB 42.95 kB +1.21% 8.86 kB 8.97 kB
oss-experimental/react-server/cjs/react-server-flight.production.js +0.53% 56.03 kB 56.33 kB +0.31% 11.14 kB 11.18 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.browser.production.js +0.52% 44.26 kB 44.49 kB +1.14% 9.23 kB 9.33 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.production.js +0.52% 44.59 kB 44.83 kB +1.14% 9.31 kB 9.42 kB
oss-experimental/react-client/cjs/react-client-flight.production.js +0.52% 45.39 kB 45.63 kB +1.03% 8.80 kB 8.89 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.node.production.js +0.49% 47.10 kB 47.33 kB +1.06% 9.89 kB 10.00 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.unbundled.production.js +0.48% 48.35 kB 48.58 kB +1.05% 10.17 kB 10.27 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.production.js +0.48% 48.35 kB 48.59 kB +1.01% 10.17 kB 10.27 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.production.js +0.47% 49.03 kB 49.26 kB +1.11% 10.34 kB 10.45 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.production.js +0.47% 49.04 kB 49.27 kB +1.14% 10.32 kB 10.44 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.production.js +0.46% 50.07 kB 50.30 kB +0.94% 10.54 kB 10.64 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.edge.production.js +0.46% 50.08 kB 50.31 kB +0.97% 10.52 kB 10.63 kB
oss-stable-semver/react-server-dom-esm/esm/react-server-dom-esm-client.browser.production.js +0.46% 46.38 kB 46.60 kB +0.75% 10.78 kB 10.87 kB
oss-stable/react-server-dom-esm/esm/react-server-dom-esm-client.browser.production.js +0.46% 46.38 kB 46.60 kB +0.75% 10.78 kB 10.87 kB
oss-stable-semver/react-client/cjs/react-client-flight.development.js +0.32% 62.77 kB 62.97 kB +0.48% 15.16 kB 15.23 kB
oss-stable/react-client/cjs/react-client-flight.development.js +0.32% 62.77 kB 62.97 kB +0.48% 15.16 kB 15.23 kB
oss-stable-semver/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js +0.32% 62.78 kB 62.98 kB +0.50% 14.87 kB 14.94 kB
oss-stable/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js +0.32% 62.78 kB 62.98 kB +0.50% 14.87 kB 14.94 kB
oss-stable-semver/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.development.js +0.32% 63.02 kB 63.22 kB +0.49% 14.93 kB 15.01 kB
oss-stable/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.development.js +0.32% 63.02 kB 63.22 kB +0.49% 14.93 kB 15.01 kB
oss-stable-semver/react-server/cjs/react-server-flight.production.js +0.32% 41.69 kB 41.83 kB +0.26% 9.11 kB 9.13 kB
oss-stable/react-server/cjs/react-server-flight.production.js +0.32% 41.69 kB 41.83 kB +0.26% 9.11 kB 9.13 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.browser.development.js +0.31% 66.27 kB 66.47 kB +0.47% 15.90 kB 15.97 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.browser.development.js +0.31% 66.27 kB 66.47 kB +0.47% 15.90 kB 15.97 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js +0.30% 66.78 kB 66.98 kB +0.46% 16.07 kB 16.14 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js +0.30% 66.78 kB 66.98 kB +0.46% 16.07 kB 16.14 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.production.js +0.29% 83.70 kB 83.94 kB +0.25% 17.26 kB 17.30 kB
oss-experimental/react-server-dom-esm/esm/react-server-dom-esm-client.browser.production.js +0.29% 69.55 kB 69.75 kB +0.50% 15.05 kB 15.13 kB
oss-stable-semver/react-server-dom-esm/cjs/react-server-dom-esm-client.node.development.js +0.29% 70.70 kB 70.90 kB +0.44% 16.97 kB 17.04 kB
oss-stable/react-server-dom-esm/cjs/react-server-dom-esm-client.node.development.js +0.29% 70.70 kB 70.90 kB +0.44% 16.97 kB 17.04 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.browser.production.js +0.29% 85.69 kB 85.93 kB +0.23% 17.38 kB 17.42 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.production.js +0.28% 86.57 kB 86.82 kB +0.34% 17.63 kB 17.69 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.js +0.28% 86.77 kB 87.02 kB +0.25% 17.62 kB 17.66 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.js +0.28% 87.30 kB 87.55 kB +0.35% 17.77 kB 17.83 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.unbundled.development.js +0.28% 72.48 kB 72.68 kB +0.42% 17.53 kB 17.60 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.unbundled.development.js +0.28% 72.48 kB 72.68 kB +0.42% 17.53 kB 17.60 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.development.js +0.28% 72.51 kB 72.72 kB +0.41% 17.55 kB 17.63 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.development.js +0.28% 72.51 kB 72.72 kB +0.41% 17.55 kB 17.63 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.unbundled.production.js +0.28% 88.50 kB 88.75 kB +0.21% 18.01 kB 18.05 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.production.js +0.27% 89.46 kB 89.71 kB +0.24% 18.22 kB 18.27 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.development.js +0.27% 73.91 kB 74.12 kB +0.39% 17.90 kB 17.97 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.development.js +0.27% 73.91 kB 74.12 kB +0.39% 17.90 kB 17.97 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.production.js +0.27% 89.60 kB 89.85 kB +0.21% 18.24 kB 18.27 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.development.js +0.27% 73.94 kB 74.14 kB +0.41% 17.94 kB 18.01 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.development.js +0.27% 73.94 kB 74.14 kB +0.41% 17.94 kB 18.01 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.production.js +0.27% 90.55 kB 90.79 kB +0.25% 18.45 kB 18.49 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.edge.development.js +0.27% 74.98 kB 75.19 kB +0.39% 18.11 kB 18.18 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.edge.development.js +0.27% 74.98 kB 75.19 kB +0.39% 18.11 kB 18.18 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.development.js +0.27% 75.01 kB 75.21 kB +0.40% 18.15 kB 18.22 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.development.js +0.27% 75.01 kB 75.21 kB +0.40% 18.15 kB 18.22 kB
oss-experimental/react-client/cjs/react-client-flight.development.js +0.22% 86.34 kB 86.53 kB +0.34% 19.53 kB 19.60 kB
oss-experimental/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js +0.22% 86.35 kB 86.54 kB +0.42% 19.24 kB 19.32 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.development.js +0.22% 86.59 kB 86.78 kB +0.42% 19.31 kB 19.39 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.browser.development.js +0.21% 89.84 kB 90.03 kB +0.39% 20.27 kB 20.35 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js +0.21% 90.35 kB 90.54 kB +0.39% 20.44 kB 20.52 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.node.development.js +0.20% 94.26 kB 94.46 kB +0.34% 21.34 kB 21.41 kB
test_utils/ReactAllWarnings.js Deleted 64.26 kB 0.00 kB Deleted 16.02 kB 0.00 kB

Generated by 🚫 dangerJS against 718e73a

@unstubbable unstubbable force-pushed the deduplicate-suspended-elements branch from 06bb3fb to 01f0a40 Compare April 4, 2024 16:09
@@ -1237,6 +1237,9 @@ function renderModel(
task.implicitSlot,
request.abortableTasks,
);
// The suspended element was outlined, so we're using the same ID for the
// original value, thus ensuring that it's deduplicated if it's referenced again.
request.writtenObjects.set((value: any), newTask.id);
Copy link
Collaborator

Choose a reason for hiding this comment

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

There's a couple of things to consider here.

  • This value might not be one of the kinds of objects that we stash in this map or even an object at all necessarily. So that would need to be confirmed.
  • The thing being executed in the task is really the model, not value. The value could be something we've already rendered past. It would be safer to do this with just model.
  • It's possible for the same element to be rendered inside two different "contexts". We removed Server Context but keyPath and implicitSlot is still a contextual thing. You could render the same element in two different key paths.

E.g. this would be safer:

if (wasReactNode && task.keyPath === null && !task.implicitSlot) {
  request.writtenObjects.set(model, newTask.id);
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

See

// If we're in some kind of context we can't necessarily reuse this object depending
// what parent components are used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

While investigating whether I could avoid the cast to any for value, I also thought I could maybe just use model instead (which, I later noticed, I would also need to cast). But it turns out that these two are not the same because of createLazyWrapperAroundWakeable, and then the deduplication does not work:

{
  value: {
    '$$typeof': Symbol(react.element),
    type: [AsyncFunction: Bar],
    key: null,
    ref: null,
    props: { text: 'bar' }
  },
  model: {
    '$$typeof': Symbol(react.lazy),
    _payload: Promise { 'BAR', status: 'pending' },
    _init: [Function: readThenable]
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So maybe this is good enough as a heuristic then?

if (wasReactNode && task.keyPath === null && !task.implicitSlot) {
  request.writtenObjects.set(value, newTask.id);
}

I'm not sure whether wasReactNode always covers value transitively. If not, we could do the same check for value, I guess?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's possible for the same element to be rendered inside two different "contexts". We removed Server Context but keyPath and implicitSlot is still a contextual thing. You could render the same element in two different key paths.

Can you expand on this a bit? Using the unit test as an example, implicitSlot is indeed set to true here for <Bar /> because it does not have a key path. Which means, using the proposed condition would only work if we move the code a couple lines further down, after the context has been restored. Would that still be correct?

@unstubbable unstubbable force-pushed the deduplicate-suspended-elements branch from 01f0a40 to 718e73a Compare May 6, 2024 07:12
@unstubbable
Copy link
Contributor Author

superseded by #28996 🥳

@unstubbable unstubbable closed this May 6, 2024
@unstubbable unstubbable deleted the deduplicate-suspended-elements branch May 6, 2024 07:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants