@samitouri / QOS-React-1 / commits / 2807d781a0

[Fizz] Reuse rootSegmentID as the SuspenseBoundaryID (#27387)

Originally the intension was to have React assign an ID to a user rendered DOM node inside a `fallback` while it was loading. If there already were an explicit `id` defined on the DOM element we would reuse that one instead. That's why this was a DOM Config option and not just built in to Fizz. This became tricky since it can load late and so we'd have to transfer it down and detect it only once it finished rendering and if there is no DOM element it doesn't work anyway. So instead, what we do in practice is to always use a `<template>` tag with the ID. This has the downside of an extra useless node and shifting child CSS selectors. Maybe we'll get around to fixing this properly but it might not be worth it. This PR just gets rid of the SuspenseBoundaryID concept and instead we just use the same ID number as the root segment ID of the boundary to refer to the boundary to simplify the implementation. This also solves the problem that SuspenseBoundaryID isn't currently serializable (although that's easily fixable by itself if necessary).

Sebastian Markbåge committed Sep 18, 2023 at 11:56 UTC 2807d781a08db8e9873687fccc25c0f12b4fb3d4
5 files changed +27 -92
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+17 -41
@@ -123,7 +123,7 @@ export type RenderState = {
123 // These can be recreated from resumable state.
124 placeholderPrefix: PrecomputedChunk,
125 segmentPrefix: PrecomputedChunk,
126 - boundaryPrefix: string,
126 + boundaryPrefix: PrecomputedChunk,
127
128 // inline script streaming format, unused if using external runtime / data
129 startInlineScript: PrecomputedChunk,
@@ -162,7 +162,7 @@ export type ResumableState = {
162 externalRuntimeScript: null | ExternalRuntimeScript, // TODO: Move to a serializable format
163 bootstrapChunks: Array<Chunk | PrecomputedChunk>, // TODO: Move to a serializable format.
164 idPrefix: string,
165 - nextSuspenseID: number,
165 + nextFormID: number,
166 streamingFormat: StreamingFormat,
167
168 // state for script streaming format, unused if using external runtime / data
@@ -272,7 +272,7 @@ export function createRenderState(
272 return {
273 placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
274 segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
275 - boundaryPrefix: idPrefix + 'B:',
275 + boundaryPrefix: stringToPrecomputedChunk(idPrefix + 'B:'),
276 startInlineScript: inlineScriptWithNonce,
277 htmlChunks: null,
278 headChunks: null,
@@ -353,7 +353,7 @@ export function createResumableState(
353 externalRuntimeScript: externalRuntimeScript,
354 bootstrapChunks: bootstrapChunks,
355 idPrefix: idPrefix,
356 - nextSuspenseID: 0,
356 + nextFormID: 0,
357 streamingFormat,
358 instructions: NothingSent,
359 hasBody: false,
@@ -605,20 +605,6 @@ export function getChildFormatContext(
605 return parentContext;
606 }
607
608 -export type SuspenseBoundaryID = null | PrecomputedChunk;
609 -
610 -export const UNINITIALIZED_SUSPENSE_BOUNDARY_ID: SuspenseBoundaryID = null;
611 -
612 -export function assignSuspenseBoundaryID(
613 - renderState: RenderState,
614 - resumableState: ResumableState,
615 -): SuspenseBoundaryID {
616 - const generatedID = resumableState.nextSuspenseID++;
617 - return stringToPrecomputedChunk(
618 - renderState.boundaryPrefix + generatedID.toString(16),
619 - );
620 -}
621 -
608 export function makeId(
609 resumableState: ResumableState,
610 treeId: string,
@@ -806,9 +792,7 @@ function pushStringAttribute(
792 }
793
794 function makeFormFieldPrefix(resumableState: ResumableState): string {
809 - // I'm just reusing this counter. It's not really the same namespace as "name".
810 - // It could just be its own counter.
811 - const id = resumableState.nextSuspenseID++;
795 + const id = resumableState.nextFormID++;
796 return resumableState.idPrefix + id;
797 }
798
@@ -3503,7 +3487,7 @@ export function writeStartCompletedSuspenseBoundary(
3487 export function writeStartPendingSuspenseBoundary(
3488 destination: Destination,
3489 renderState: RenderState,
3506 - id: SuspenseBoundaryID,
3490 + id: number,
3491 ): boolean {
3492 writeChunk(destination, startPendingSuspenseBoundary1);
3493
@@ -3513,7 +3497,8 @@ export function writeStartPendingSuspenseBoundary(
3497 );
3498 }
3499
3516 - writeChunk(destination, id);
3500 + writeChunk(destination, renderState.boundaryPrefix);
3501 + writeChunk(destination, stringToChunk(id.toString(16)));
3502 return writeChunkAndReturn(destination, startPendingSuspenseBoundary2);
3503 }
3504 export function writeStartClientRenderedSuspenseBoundary(
@@ -3807,8 +3792,7 @@ export function writeCompletedBoundaryInstruction(
3792 destination: Destination,
3793 resumableState: ResumableState,
3794 renderState: RenderState,
3810 - boundaryID: SuspenseBoundaryID,
3811 - contentSegmentID: number,
3795 + id: number,
3796 boundaryResources: BoundaryResources,
3797 ): boolean {
3798 let requiresStyleInsertion;
@@ -3864,22 +3848,19 @@ export function writeCompletedBoundaryInstruction(
3848 }
3849 }
3850
3867 - if (boundaryID === null) {
3868 - throw new Error(
3869 - 'An ID must have been assigned before we can complete the boundary.',
3870 - );
3871 - }
3851 + const idChunk = stringToChunk(id.toString(16));
3852 +
3853 + writeChunk(destination, renderState.boundaryPrefix);
3854 + writeChunk(destination, idChunk);
3855
3856 // Write function arguments, which are string and array literals
3874 - const formattedContentID = stringToChunk(contentSegmentID.toString(16));
3875 - writeChunk(destination, boundaryID);
3857 if (scriptFormat) {
3858 writeChunk(destination, completeBoundaryScript2);
3859 } else {
3860 writeChunk(destination, completeBoundaryData2);
3861 }
3862 writeChunk(destination, renderState.segmentPrefix);
3882 - writeChunk(destination, formattedContentID);
3863 + writeChunk(destination, idChunk);
3864 if (enableFloat && requiresStyleInsertion) {
3865 // Script and data writers must format this differently:
3866 // - script writer emits an array literal, whose string elements are
@@ -3928,7 +3909,7 @@ export function writeClientRenderBoundaryInstruction(
3909 destination: Destination,
3910 resumableState: ResumableState,
3911 renderState: RenderState,
3931 - boundaryID: SuspenseBoundaryID,
3912 + id: number,
3913 errorDigest: ?string,
3914 errorMessage?: string,
3915 errorComponentStack?: string,
@@ -3954,13 +3935,8 @@ export function writeClientRenderBoundaryInstruction(
3935 writeChunk(destination, clientRenderData1);
3936 }
3937
3957 - if (boundaryID === null) {
3958 - throw new Error(
3959 - 'An ID must have been assigned before we can complete the boundary.',
3960 - );
3961 - }
3962 -
3963 - writeChunk(destination, boundaryID);
3938 + writeChunk(destination, renderState.boundaryPrefix);
3939 + writeChunk(destination, stringToChunk(id.toString(16)));
3940 if (scriptFormat) {
3941 // " needs to be inserted for scripts, since ArgInterstitual does not contain
3942 // leading or trailing quotes
packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
+1 -4
@@ -35,7 +35,7 @@ export type RenderState = {
35 // Keep this in sync with ReactFizzConfigDOM
36 placeholderPrefix: PrecomputedChunk,
37 segmentPrefix: PrecomputedChunk,
38 - boundaryPrefix: string,
38 + boundaryPrefix: PrecomputedChunk,
39 startInlineScript: PrecomputedChunk,
40 htmlChunks: null | Array<Chunk | PrecomputedChunk>,
41 headChunks: null | Array<Chunk | PrecomputedChunk>,
@@ -89,13 +89,10 @@ export type {
89 ResumableState,
90 BoundaryResources,
91 FormatContext,
92 - SuspenseBoundaryID,
92 } from './ReactFizzConfigDOM';
93
94 export {
95 getChildFormatContext,
97 - UNINITIALIZED_SUSPENSE_BOUNDARY_ID,
98 - assignSuspenseBoundaryID,
96 makeId,
97 pushStartInstance,
98 pushEndInstance,
packages/react-noop-renderer/src/ReactNoopServer.js
-7
@@ -90,13 +90,6 @@ const ReactNoopServer = ReactFizzServer({
90 closeWithError(destination: Destination, error: mixed): void {},
91 flushBuffered(destination: Destination): void {},
92
93 - UNINITIALIZED_SUSPENSE_BOUNDARY_ID: null,
94 -
95 - assignSuspenseBoundaryID(): SuspenseInstance {
96 - // The ID is a pointer to the boundary itself.
97 - return {state: 'pending', children: []};
98 - },
99 -
93 getChildFormatContext(): null {
94 return null;
95 },
packages/react-server/src/ReactFizzServer.js
+9 -36
@@ -23,7 +23,6 @@ import type {
23 } from 'shared/ReactTypes';
24 import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
25 import type {
26 - SuspenseBoundaryID,
26 RenderState,
27 ResumableState,
28 FormatContext,
@@ -64,8 +63,6 @@ import {
63 pushStartCompletedSuspenseBoundary,
64 pushEndCompletedSuspenseBoundary,
65 pushSegmentFinale,
67 - UNINITIALIZED_SUSPENSE_BOUNDARY_ID,
68 - assignSuspenseBoundaryID,
66 getChildFormatContext,
67 writeResourcesForBoundary,
68 writePreamble,
@@ -176,7 +173,6 @@ type ReplaySuspenseBoundary = [
173 string | null /* name */,
174 string | number /* key */,
175 Array<ResumableNode> /* children */,
179 - SuspenseBoundaryID /* id */,
176 number /* rootSegmentID */,
177 ];
178
@@ -193,7 +189,6 @@ type ResumeSuspenseBoundary = [
189 3, // RESUME_SUSPENSE_BOUNDARY
190 string | null /* name */,
191 string | number /* key */,
196 - SuspenseBoundaryID /* id */,
192 number /* rootSegmentID */,
193 ];
194
@@ -229,7 +224,6 @@ const CLIENT_RENDERED = 4; // if it errors or infinitely suspends
224
225 type SuspenseBoundary = {
226 status: 0 | 1 | 4 | 5,
232 - id: SuspenseBoundaryID,
227 rootSegmentID: number,
228 errorDigest: ?string, // the error hash if it errors
229 errorMessage?: string, // the error string if it errors
@@ -575,7 +569,6 @@ function createSuspenseBoundary(
569 ): SuspenseBoundary {
570 return {
571 status: PENDING,
578 - id: UNINITIALIZED_SUSPENSE_BOUNDARY_ID,
572 rootSegmentID: -1,
573 parentFlushed: false,
574 pendingTasks: 0,
@@ -1004,8 +997,7 @@ function replaySuspenseBoundary(
997 );
998 resumedBoundary.parentFlushed = true;
999 // We restore the same id of this boundary as was used during prerender.
1007 - resumedBoundary.id = replayNode[4];
1008 - resumedBoundary.rootSegmentID = replayNode[5];
1000 + resumedBoundary.rootSegmentID = replayNode[4];
1001
1002 // We can reuse the current context and task to render the content immediately without
1003 // context switching. We just need to temporarily switch which boundary and replay node
@@ -1102,9 +1094,9 @@ function resumeSuspenseBoundary(
1094 task.keyPath,
1095 );
1096 resumedBoundary.parentFlushed = true;
1097 + const id = replayNode[3];
1098 // We restore the same id of this boundary as was used during prerender.
1106 - resumedBoundary.id = replayNode[3];
1107 - resumedBoundary.rootSegmentID = replayNode[4];
1099 + resumedBoundary.rootSegmentID = id;
1100
1101 const resumedSegment = createPendingSegment(
1102 request,
@@ -1115,7 +1107,7 @@ function resumeSuspenseBoundary(
1107 false,
1108 );
1109 resumedSegment.parentFlushed = true;
1118 - resumedSegment.id = replayNode[4];
1110 + resumedSegment.id = id;
1111
1112 // We can reuse the current context and task to render the content immediately without
1113 // context switching. We just need to temporarily switch which boundary and replay node
@@ -2647,10 +2639,6 @@ function trackPostpone(
2639 boundary.status = POSTPONED;
2640 // We need to eagerly assign it an ID because we'll need to refer to
2641 // it before flushing and we know that we can't inline it.
2650 - boundary.id = assignSuspenseBoundaryID(
2651 - request.renderState,
2652 - request.resumableState,
2653 - );
2642 boundary.rootSegmentID = request.nextSegmentId++;
2643
2644 const boundaryKeyPath = boundary.keyPath;
@@ -2669,7 +2657,6 @@ function trackPostpone(
2657 RESUME_SUSPENSE_BOUNDARY,
2658 boundaryKeyPath[1],
2659 boundaryKeyPath[2],
2672 - boundary.id,
2660 boundary.rootSegmentID,
2661 ];
2662 addToReplayParent(boundaryNode, boundaryKeyPath[0], trackedPostpones);
@@ -2681,7 +2668,6 @@ function trackPostpone(
2668 boundaryKeyPath[1],
2669 boundaryKeyPath[2],
2670 children,
2684 - boundary.id,
2671 boundary.rootSegmentID,
2672 ];
2673 trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode);
@@ -3098,7 +3084,6 @@ function abortTaskSoft(this: Request, task: Task): void {
3084
3085 function abortRemainingSuspenseBoundary(
3086 request: Request,
3101 - id: SuspenseBoundaryID,
3087 rootSegmentID: number,
3088 error: mixed,
3089 errorDigest: ?string,
@@ -3110,7 +3095,6 @@ function abortRemainingSuspenseBoundary(
3095 );
3096 resumedBoundary.parentFlushed = true;
3097 // We restore the same id of this boundary as was used during prerender.
3113 - resumedBoundary.id = id;
3098 resumedBoundary.rootSegmentID = rootSegmentID;
3099
3100 resumedBoundary.status = CLIENT_RENDERED;
@@ -3159,11 +3143,9 @@ function abortRemainingResumableNodes(
3143 }
3144 case REPLAY_SUSPENSE_BOUNDARY: {
3145 const boundaryNode: ReplaySuspenseBoundary = node;
3162 - const id = boundaryNode[4];
3163 - const rootSegmentID = boundaryNode[5];
3146 + const rootSegmentID = boundaryNode[4];
3147 abortRemainingSuspenseBoundary(
3148 request,
3166 - id,
3149 rootSegmentID,
3150 error,
3151 errorDigest,
@@ -3172,11 +3154,9 @@ function abortRemainingResumableNodes(
3154 }
3155 case RESUME_SUSPENSE_BOUNDARY: {
3156 const boundaryNode: ResumeSuspenseBoundary = node;
3175 - const id = boundaryNode[3];
3176 - const rootSegmentID = boundaryNode[4];
3157 + const rootSegmentID = boundaryNode[3];
3158 abortRemainingSuspenseBoundary(
3159 request,
3179 - id,
3160 rootSegmentID,
3161 error,
3162 errorDigest,
@@ -3753,10 +3733,6 @@ function flushSegment(
3733 if (boundary.status === PENDING) {
3734 // For pending boundaries we lazily assign an ID to the boundary
3735 // and root segment.
3756 - boundary.id = assignSuspenseBoundaryID(
3757 - request.renderState,
3758 - request.resumableState,
3759 - );
3736 boundary.rootSegmentID = request.nextSegmentId++;
3737 }
3738
@@ -3767,9 +3743,7 @@ function flushSegment(
3743
3744 // This boundary is still loading. Emit a pending suspense boundary wrapper.
3745
3770 - /// This is the first time we should have referenced this ID.
3771 - const id = boundary.id;
3772 -
3746 + const id = boundary.rootSegmentID;
3747 writeStartPendingSuspenseBoundary(destination, request.renderState, id);
3748
3749 // Flush the fallback.
@@ -3791,7 +3765,7 @@ function flushSegment(
3765 writeStartPendingSuspenseBoundary(
3766 destination,
3767 request.renderState,
3794 - boundary.id,
3768 + boundary.rootSegmentID,
3769 );
3770
3771 // Flush the fallback.
@@ -3829,7 +3803,7 @@ function flushClientRenderedBoundary(
3803 destination,
3804 request.resumableState,
3805 request.renderState,
3832 - boundary.id,
3806 + boundary.rootSegmentID,
3807 boundary.errorDigest,
3808 boundary.errorMessage,
3809 boundary.errorComponentStack,
@@ -3882,7 +3856,6 @@ function flushCompletedBoundary(
3856 destination,
3857 request.resumableState,
3858 request.renderState,
3885 - boundary.id,
3859 boundary.rootSegmentID,
3860 boundary.resources,
3861 );
packages/react-server/src/forks/ReactFizzConfig.custom.js
-4
@@ -32,7 +32,6 @@ export opaque type RenderState = mixed;
32 export opaque type ResumableState = mixed;
33 export opaque type BoundaryResources = mixed;
34 export opaque type FormatContext = mixed;
35 -export opaque type SuspenseBoundaryID = mixed;
35 export type {TransitionStatus};
36
37 export const isPrimaryRenderer = false;
@@ -41,9 +40,6 @@ export const supportsRequestStorage = false;
40 export const requestStorage: AsyncLocalStorage<Request> = (null: any);
41
42 export const getChildFormatContext = $$$config.getChildFormatContext;
44 -export const UNINITIALIZED_SUSPENSE_BOUNDARY_ID =
45 - $$$config.UNINITIALIZED_SUSPENSE_BOUNDARY_ID;
46 -export const assignSuspenseBoundaryID = $$$config.assignSuspenseBoundaryID;
43 export const makeId = $$$config.makeId;
44 export const pushTextInstance = $$$config.pushTextInstance;
45 export const pushStartInstance = $$$config.pushStartInstance;