@samitouri / QOS-React-2 / commits / 4f93170066

[Flight] Cache the value if we visit the same I/O or Promise multiple times along different paths (#35005)

We avoid visiting the same async node twice but if we see it again we returned "null" indicating that there's no I/O there. This means that if you have two different Promises both resolving from the same I/O node then we only show one of them. However, in general we treat that as two different I/O entries to allow for things like batching to still show up separately. This fixes that by caching the return value for multiple visits. So if we found I/O (but no user space await) in one path and then we visit that path through a different Promise chain, then we'll still emit it twice. However, if we visit the same exact Promise that we emitted an await on then we skip it. Because there's no need to emit two awaits on the same thing. It only matters when the path ends up informing whether it has I/O or not.

Sebastian Markbåge committed Oct 29, 2025 at 10:55 UTC 4f93170066c5ee7519749b45c5962a6b970cf977
1 file changed +36 -7
packages/react-server/src/ReactFlightServer.js
+36 -7
@@ -2316,15 +2316,37 @@ function visitAsyncNode(
2316 request: Request,
2317 task: Task,
2318 node: AsyncSequence,
2319 - visited: Set<AsyncSequence | ReactDebugInfo>,
2319 + visited: Map<
2320 + AsyncSequence | ReactDebugInfo,
2321 + void | null | PromiseNode | IONode,
2322 + >,
2323 cutOff: number,
2324 ): void | null | PromiseNode | IONode {
2325 if (visited.has(node)) {
2326 // It's possible to visit them same node twice when it's part of both an "awaited" path
2327 // and a "previous" path. This also gracefully handles cycles which would be a bug.
2325 - return null;
2328 + return visited.get(node);
2329 + }
2330 + // Set it as visited early in case we see ourselves before returning.
2331 + visited.set(node, null);
2332 + const result = visitAsyncNodeImpl(request, task, node, visited, cutOff);
2333 + if (result !== null) {
2334 + // If we ended up with a value, let's use that value for future visits.
2335 + visited.set(node, result);
2336 }
2327 - visited.add(node);
2337 + return result;
2338 +}
2339 +
2340 +function visitAsyncNodeImpl(
2341 + request: Request,
2342 + task: Task,
2343 + node: AsyncSequence,
2344 + visited: Map<
2345 + AsyncSequence | ReactDebugInfo,
2346 + void | null | PromiseNode | IONode,
2347 + >,
2348 + cutOff: number,
2349 +): void | null | PromiseNode | IONode {
2350 if (node.end >= 0 && node.end <= request.timeOrigin) {
2351 // This was already resolved when we started this render. It must have been either something
2352 // that's part of a start up sequence or externally cached data. We exclude that information.
@@ -2416,7 +2438,7 @@ function visitAsyncNode(
2438 if (promise !== undefined) {
2439 const debugInfo = promise._debugInfo;
2440 if (debugInfo != null && !visited.has(debugInfo)) {
2419 - visited.add(debugInfo);
2441 + visited.set(debugInfo, null);
2442 forwardDebugInfo(request, task, debugInfo);
2443 }
2444 }
@@ -2483,6 +2505,10 @@ function visitAsyncNode(
2505 // Promise that was ultimately awaited by the user space await.
2506 serializeIONode(request, ioNode, awaited.promise);
2507
2508 + // If we ever visit this I/O node again, skip it because we already emitted this
2509 + // exact entry and we don't need two awaits on the same thing.
2510 + visited.set(ioNode, null);
2511 +
2512 // Ensure the owner is already outlined.
2513 if (node.owner != null) {
2514 outlineComponentInfo(request, node.owner);
@@ -2521,7 +2547,7 @@ function visitAsyncNode(
2547 if (promise !== undefined) {
2548 const debugInfo = promise._debugInfo;
2549 if (debugInfo != null && !visited.has(debugInfo)) {
2524 - visited.add(debugInfo);
2550 + visited.set(debugInfo, null);
2551 forwardDebugInfo(request, task, debugInfo);
2552 }
2553 }
@@ -2542,9 +2568,12 @@ function emitAsyncSequence(
2568 owner: null | ReactComponentInfo,
2569 stack: null | Error,
2570 ): void {
2545 - const visited: Set<AsyncSequence | ReactDebugInfo> = new Set();
2571 + const visited: Map<
2572 + AsyncSequence | ReactDebugInfo,
2573 + void | null | PromiseNode | IONode,
2574 + > = new Map();
2575 if (__DEV__ && alreadyForwardedDebugInfo) {
2547 - visited.add(alreadyForwardedDebugInfo);
2576 + visited.set(alreadyForwardedDebugInfo, null);
2577 }
2578 const awaitedNode = visitAsyncNode(request, task, node, visited, task.time);
2579 if (awaitedNode === undefined) {