@samitouri / QOS-React-2 / commits / 3f67d0857e

[Fizz] Track whether we're in a fallback on FormatContext (#33194)

Removes the `isFallback` flag on Tasks and tracks it on the formatContext instead. Less memory and avoids passing and tracking extra arguments to all the pushStartInstance branches that doesn't need it. We'll need to be able to track more Suspense related contexts on this for View Transitions anyway.

Sebastian Markbåge committed May 15, 2025 at 00:06 UTC 3f67d0857efc3ab21b9d30851f5a8451471166ab
6 files changed +88 -86
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+58 -65
@@ -741,9 +741,10 @@ const HTML_COLGROUP_MODE = 9;
741
742 type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
743
744 -const NO_SCOPE = /* */ 0b00;
745 -const NOSCRIPT_SCOPE = /* */ 0b01;
746 -const PICTURE_SCOPE = /* */ 0b10;
744 +const NO_SCOPE = /* */ 0b000;
745 +const NOSCRIPT_SCOPE = /* */ 0b001;
746 +const PICTURE_SCOPE = /* */ 0b010;
747 +const FALLBACK_SCOPE = /* */ 0b100;
748
749 // Lets us keep track of contextual state and pick it back up after suspending.
750 export type FormatContext = {
@@ -754,7 +755,7 @@ export type FormatContext = {
755
756 function createFormatContext(
757 insertionMode: InsertionMode,
757 - selectedValue: null | string,
758 + selectedValue: null | string | Array<string>,
759 tagScope: number,
760 ): FormatContext {
761 return {
@@ -864,6 +865,22 @@ export function getChildFormatContext(
865 return parentContext;
866 }
867
868 +export function getSuspenseFallbackFormatContext(
869 + parentContext: FormatContext,
870 +): FormatContext {
871 + return createFormatContext(
872 + parentContext.insertionMode,
873 + parentContext.selectedValue,
874 + parentContext.tagScope | FALLBACK_SCOPE,
875 + );
876 +}
877 +
878 +export function getSuspenseContentFormatContext(
879 + parentContext: FormatContext,
880 +): FormatContext {
881 + return parentContext;
882 +}
883 +
884 export function isPreambleContext(formatContext: FormatContext): boolean {
885 return formatContext.insertionMode === HTML_HEAD_MODE;
886 }
@@ -2511,12 +2528,12 @@ function pushMeta(
2528 props: Object,
2529 renderState: RenderState,
2530 textEmbedded: boolean,
2514 - insertionMode: InsertionMode,
2515 - noscriptTagInScope: boolean,
2516 - isFallback: boolean,
2531 + formatContext: FormatContext,
2532 ): null {
2533 + const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE;
2534 + const isFallback = formatContext.tagScope & FALLBACK_SCOPE;
2535 if (
2519 - insertionMode === SVG_MODE ||
2536 + formatContext.insertionMode === SVG_MODE ||
2537 noscriptTagInScope ||
2538 props.itemProp != null
2539 ) {
@@ -2559,15 +2576,15 @@ function pushLink(
2576 renderState: RenderState,
2577 hoistableState: null | HoistableState,
2578 textEmbedded: boolean,
2562 - insertionMode: InsertionMode,
2563 - noscriptTagInScope: boolean,
2564 - isFallback: boolean,
2579 + formatContext: FormatContext,
2580 ): null {
2581 + const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE;
2582 + const isFallback = formatContext.tagScope & FALLBACK_SCOPE;
2583 const rel = props.rel;
2584 const href = props.href;
2585 const precedence = props.precedence;
2586 if (
2570 - insertionMode === SVG_MODE ||
2587 + formatContext.insertionMode === SVG_MODE ||
2588 noscriptTagInScope ||
2589 props.itemProp != null ||
2590 typeof rel !== 'string' ||
@@ -2765,9 +2782,9 @@ function pushStyle(
2782 renderState: RenderState,
2783 hoistableState: null | HoistableState,
2784 textEmbedded: boolean,
2768 - insertionMode: InsertionMode,
2769 - noscriptTagInScope: boolean,
2785 + formatContext: FormatContext,
2786 ): ReactNodeList {
2787 + const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE;
2788 if (__DEV__) {
2789 if (hasOwnProperty.call(props, 'children')) {
2790 const children = props.children;
@@ -2801,7 +2818,7 @@ function pushStyle(
2818 const href = props.href;
2819
2820 if (
2804 - insertionMode === SVG_MODE ||
2821 + formatContext.insertionMode === SVG_MODE ||
2822 noscriptTagInScope ||
2823 props.itemProp != null ||
2824 typeof precedence !== 'string' ||
@@ -2984,8 +3001,10 @@ function pushImg(
3001 props: Object,
3002 resumableState: ResumableState,
3003 renderState: RenderState,
2987 - pictureOrNoScriptTagInScope: boolean,
3004 + formatContext: FormatContext,
3005 ): null {
3006 + const pictureOrNoScriptTagInScope =
3007 + formatContext.tagScope & (PICTURE_SCOPE | NOSCRIPT_SCOPE);
3008 const {src, srcSet} = props;
3009 if (
3010 props.loading !== 'lazy' &&
@@ -2993,7 +3012,7 @@ function pushImg(
3012 (typeof src === 'string' || src == null) &&
3013 (typeof srcSet === 'string' || srcSet == null) &&
3014 props.fetchPriority !== 'low' &&
2996 - pictureOrNoScriptTagInScope === false &&
3015 + !pictureOrNoScriptTagInScope &&
3016 // We exclude data URIs in src and srcSet since these should not be preloaded
3017 !(
3018 typeof src === 'string' &&
@@ -3190,10 +3209,10 @@ function pushTitle(
3209 target: Array<Chunk | PrecomputedChunk>,
3210 props: Object,
3211 renderState: RenderState,
3193 - insertionMode: InsertionMode,
3194 - noscriptTagInScope: boolean,
3195 - isFallback: boolean,
3212 + formatContext: FormatContext,
3213 ): ReactNodeList {
3214 + const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE;
3215 + const isFallback = formatContext.tagScope & FALLBACK_SCOPE;
3216 if (__DEV__) {
3217 if (hasOwnProperty.call(props, 'children')) {
3218 const children = props.children;
@@ -3243,7 +3262,7 @@ function pushTitle(
3262 }
3263
3264 if (
3246 - insertionMode !== SVG_MODE &&
3265 + formatContext.insertionMode !== SVG_MODE &&
3266 !noscriptTagInScope &&
3267 props.itemProp == null
3268 ) {
@@ -3320,9 +3339,9 @@ function pushStartHead(
3339 props: Object,
3340 renderState: RenderState,
3341 preambleState: null | PreambleState,
3323 - insertionMode: InsertionMode,
3342 + formatContext: FormatContext,
3343 ): ReactNodeList {
3325 - if (insertionMode < HTML_MODE) {
3344 + if (formatContext.insertionMode < HTML_MODE) {
3345 // This <head> is the Document.head and should be part of the preamble
3346 const preamble = preambleState || renderState.preamble;
3347
@@ -3349,9 +3368,9 @@ function pushStartBody(
3368 props: Object,
3369 renderState: RenderState,
3370 preambleState: null | PreambleState,
3352 - insertionMode: InsertionMode,
3371 + formatContext: FormatContext,
3372 ): ReactNodeList {
3354 - if (insertionMode < HTML_MODE) {
3373 + if (formatContext.insertionMode < HTML_MODE) {
3374 // This <body> is the Document.body
3375 const preamble = preambleState || renderState.preamble;
3376
@@ -3378,9 +3397,9 @@ function pushStartHtml(
3397 props: Object,
3398 renderState: RenderState,
3399 preambleState: null | PreambleState,
3381 - insertionMode: InsertionMode,
3400 + formatContext: FormatContext,
3401 ): ReactNodeList {
3383 - if (insertionMode === ROOT_HTML_MODE) {
3402 + if (formatContext.insertionMode === ROOT_HTML_MODE) {
3403 // This <html> is the Document.documentElement
3404 const preamble = preambleState || renderState.preamble;
3405
@@ -3408,9 +3427,9 @@ function pushScript(
3427 resumableState: ResumableState,
3428 renderState: RenderState,
3429 textEmbedded: boolean,
3411 - insertionMode: InsertionMode,
3412 - noscriptTagInScope: boolean,
3430 + formatContext: FormatContext,
3431 ): null {
3432 + const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE;
3433 const asyncProp = props.async;
3434 if (
3435 typeof props.src !== 'string' ||
@@ -3422,7 +3441,7 @@ function pushScript(
3441 ) ||
3442 props.onLoad ||
3443 props.onError ||
3425 - insertionMode === SVG_MODE ||
3444 + formatContext.insertionMode === SVG_MODE ||
3445 noscriptTagInScope ||
3446 props.itemProp != null
3447 ) {
@@ -3790,7 +3809,6 @@ export function pushStartInstance(
3809 hoistableState: null | HoistableState,
3810 formatContext: FormatContext,
3811 textEmbedded: boolean,
3793 - isFallback: boolean,
3812 ): ReactNodeList {
3813 if (__DEV__) {
3814 validateARIAProperties(type, props);
@@ -3857,14 +3875,7 @@ export function pushStartInstance(
3875 case 'object':
3876 return pushStartObject(target, props);
3877 case 'title':
3860 - return pushTitle(
3861 - target,
3862 - props,
3863 - renderState,
3864 - formatContext.insertionMode,
3865 - !!(formatContext.tagScope & NOSCRIPT_SCOPE),
3866 - isFallback,
3867 - );
3878 + return pushTitle(target, props, renderState, formatContext);
3879 case 'link':
3880 return pushLink(
3881 target,
@@ -3873,9 +3884,7 @@ export function pushStartInstance(
3884 renderState,
3885 hoistableState,
3886 textEmbedded,
3876 - formatContext.insertionMode,
3877 - !!(formatContext.tagScope & NOSCRIPT_SCOPE),
3878 - isFallback,
3887 + formatContext,
3888 );
3889 case 'script':
3890 return pushScript(
@@ -3884,8 +3893,7 @@ export function pushStartInstance(
3893 resumableState,
3894 renderState,
3895 textEmbedded,
3887 - formatContext.insertionMode,
3888 - !!(formatContext.tagScope & NOSCRIPT_SCOPE),
3896 + formatContext,
3897 );
3898 case 'style':
3899 return pushStyle(
@@ -3895,32 +3903,17 @@ export function pushStartInstance(
3903 renderState,
3904 hoistableState,
3905 textEmbedded,
3898 - formatContext.insertionMode,
3899 - !!(formatContext.tagScope & NOSCRIPT_SCOPE),
3906 + formatContext,
3907 );
3908 case 'meta':
3902 - return pushMeta(
3903 - target,
3904 - props,
3905 - renderState,
3906 - textEmbedded,
3907 - formatContext.insertionMode,
3908 - !!(formatContext.tagScope & NOSCRIPT_SCOPE),
3909 - isFallback,
3910 - );
3909 + return pushMeta(target, props, renderState, textEmbedded, formatContext);
3910 // Newline eating tags
3911 case 'listing':
3912 case 'pre': {
3913 return pushStartPreformattedElement(target, props, type);
3914 }
3915 case 'img': {
3917 - return pushImg(
3918 - target,
3919 - props,
3920 - resumableState,
3921 - renderState,
3922 - !!(formatContext.tagScope & (PICTURE_SCOPE | NOSCRIPT_SCOPE)),
3923 - );
3916 + return pushImg(target, props, resumableState, renderState, formatContext);
3917 }
3918 // Omitted close tags
3919 case 'base':
@@ -3955,7 +3948,7 @@ export function pushStartInstance(
3948 props,
3949 renderState,
3950 preambleState,
3958 - formatContext.insertionMode,
3951 + formatContext,
3952 );
3953 case 'body':
3954 return pushStartBody(
@@ -3963,7 +3956,7 @@ export function pushStartInstance(
3956 props,
3957 renderState,
3958 preambleState,
3966 - formatContext.insertionMode,
3959 + formatContext,
3960 );
3961 case 'html': {
3962 return pushStartHtml(
@@ -3971,7 +3964,7 @@ export function pushStartInstance(
3964 props,
3965 renderState,
3966 preambleState,
3974 - formatContext.insertionMode,
3967 + formatContext,
3968 );
3969 }
3970 default: {
packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
+2
@@ -141,6 +141,8 @@ export type {
141
142 export {
143 getChildFormatContext,
144 + getSuspenseFallbackFormatContext,
145 + getSuspenseContentFormatContext,
146 makeId,
147 pushStartInstance,
148 pushEndInstance,
packages/react-markup/src/ReactFizzConfigMarkup.js
+2 -2
@@ -52,6 +52,8 @@ export type {
52
53 export {
54 getChildFormatContext,
55 + getSuspenseFallbackFormatContext,
56 + getSuspenseContentFormatContext,
57 makeId,
58 pushEndInstance,
59 pushFormStateMarkerIsMatching,
@@ -96,7 +98,6 @@ export function pushStartInstance(
98 hoistableState: null | HoistableState,
99 formatContext: FormatContext,
100 textEmbedded: boolean,
99 - isFallback: boolean,
101 ): ReactNodeList {
102 for (const propKey in props) {
103 if (hasOwnProperty.call(props, propKey)) {
@@ -127,7 +128,6 @@ export function pushStartInstance(
128 hoistableState,
129 formatContext,
130 textEmbedded,
130 - isFallback,
131 );
132 }
133
packages/react-noop-renderer/src/ReactNoopServer.js
+6
@@ -104,6 +104,12 @@ const ReactNoopServer = ReactFizzServer({
104 getChildFormatContext(): null {
105 return null;
106 },
107 + getSuspenseFallbackFormatContext(): null {
108 + return null;
109 + },
110 + getSuspenseContentFormatContext(): null {
111 + return null;
112 + },
113
114 resetResumableState(): void {},
115 completeResumableState(): void {},
packages/react-server/src/ReactFizzServer.js
+16 -19
@@ -74,6 +74,8 @@ import {
74 pushEndInstance,
75 pushSegmentFinale,
76 getChildFormatContext,
77 + getSuspenseFallbackFormatContext,
78 + getSuspenseContentFormatContext,
79 writeHoistables,
80 writePreambleStart,
81 writePreambleEnd,
@@ -263,7 +265,6 @@ type RenderTask = {
265 treeContext: TreeContext, // the current tree context that this task is executing in
266 componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component
267 thenableState: null | ThenableState,
266 - isFallback: boolean, // whether this task is rendering inside a fallback tree
268 legacyContext: LegacyContext, // the current legacy context that this task is executing in
269 debugTask: null | ConsoleTask, // DEV only
270 // DON'T ANY MORE FIELDS. We at 16 already which otherwise requires converting to a constructor.
@@ -294,7 +295,6 @@ type ReplayTask = {
295 treeContext: TreeContext, // the current tree context that this task is executing in
296 componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component
297 thenableState: null | ThenableState,
297 - isFallback: boolean, // whether this task is rendering inside a fallback tree
298 legacyContext: LegacyContext, // the current legacy context that this task is executing in
299 debugTask: null | ConsoleTask, // DEV only
300 // DON'T ANY MORE FIELDS. We at 16 already which otherwise requires converting to a constructor.
@@ -537,7 +537,6 @@ export function createRequest(
537 rootContextSnapshot,
538 emptyTreeContext,
539 null,
540 - false,
540 emptyContextObject,
541 null,
542 );
@@ -643,7 +642,6 @@ export function resumeRequest(
642 rootContextSnapshot,
643 emptyTreeContext,
644 null,
646 - false,
645 emptyContextObject,
646 null,
647 );
@@ -671,7 +669,6 @@ export function resumeRequest(
669 rootContextSnapshot,
670 emptyTreeContext,
671 null,
674 - false,
672 emptyContextObject,
673 null,
674 );
@@ -781,7 +778,6 @@ function createRenderTask(
778 context: ContextSnapshot,
779 treeContext: TreeContext,
780 componentStack: null | ComponentStackNode,
784 - isFallback: boolean,
781 legacyContext: LegacyContext,
782 debugTask: null | ConsoleTask,
783 ): RenderTask {
@@ -807,7 +803,6 @@ function createRenderTask(
803 treeContext,
804 componentStack,
805 thenableState,
810 - isFallback,
806 }: any);
807 if (!disableLegacyContext) {
808 task.legacyContext = legacyContext;
@@ -833,7 +828,6 @@ function createReplayTask(
828 context: ContextSnapshot,
829 treeContext: TreeContext,
830 componentStack: null | ComponentStackNode,
836 - isFallback: boolean,
831 legacyContext: LegacyContext,
832 debugTask: null | ConsoleTask,
833 ): ReplayTask {
@@ -860,7 +854,6 @@ function createReplayTask(
854 treeContext,
855 componentStack,
856 thenableState,
863 - isFallback,
857 }: any);
858 if (!disableLegacyContext) {
859 task.legacyContext = legacyContext;
@@ -1146,12 +1139,15 @@ function renderSuspenseBoundary(
1139 // an already completed Suspense boundary. It's too late to do anything about it
1140 // so we can just render through it.
1141 const prevKeyPath = someTask.keyPath;
1142 + const prevContext = someTask.formatContext;
1143 someTask.keyPath = keyPath;
1144 + someTask.formatContext = getSuspenseContentFormatContext(prevContext);
1145 const content: ReactNodeList = props.children;
1146 try {
1147 renderNode(request, someTask, content, -1);
1148 } finally {
1149 someTask.keyPath = prevKeyPath;
1150 + someTask.formatContext = prevContext;
1151 }
1152 return;
1153 }
@@ -1159,6 +1155,7 @@ function renderSuspenseBoundary(
1155 const task: RenderTask = someTask;
1156
1157 const prevKeyPath = task.keyPath;
1158 + const prevContext = task.formatContext;
1159 const parentBoundary = task.blockedBoundary;
1160 const parentPreamble = task.blockedPreamble;
1161 const parentHoistableState = task.hoistableState;
@@ -1237,6 +1234,7 @@ function renderSuspenseBoundary(
1234 task.blockedSegment = boundarySegment;
1235 task.blockedPreamble = newBoundary.fallbackPreamble;
1236 task.keyPath = fallbackKeyPath;
1237 + task.formatContext = getSuspenseFallbackFormatContext(prevContext);
1238 boundarySegment.status = RENDERING;
1239 try {
1240 renderNode(request, task, fallback, -1);
@@ -1259,6 +1257,7 @@ function renderSuspenseBoundary(
1257 task.blockedSegment = parentSegment;
1258 task.blockedPreamble = parentPreamble;
1259 task.keyPath = prevKeyPath;
1260 + task.formatContext = prevContext;
1261 }
1262
1263 // We create a suspended task for the primary content because we want to allow
@@ -1274,11 +1273,10 @@ function renderSuspenseBoundary(
1273 newBoundary.contentState,
1274 task.abortSet,
1275 keyPath,
1277 - task.formatContext,
1276 + getSuspenseContentFormatContext(task.formatContext),
1277 task.context,
1278 task.treeContext,
1279 task.componentStack,
1281 - task.isFallback,
1280 !disableLegacyContext ? task.legacyContext : emptyContextObject,
1281 __DEV__ ? task.debugTask : null,
1282 );
@@ -1302,6 +1300,7 @@ function renderSuspenseBoundary(
1300 task.hoistableState = newBoundary.contentState;
1301 task.blockedSegment = contentRootSegment;
1302 task.keyPath = keyPath;
1303 + task.formatContext = getSuspenseContentFormatContext(prevContext);
1304 contentRootSegment.status = RENDERING;
1305
1306 try {
@@ -1388,6 +1387,7 @@ function renderSuspenseBoundary(
1387 task.hoistableState = parentHoistableState;
1388 task.blockedSegment = parentSegment;
1389 task.keyPath = prevKeyPath;
1390 + task.formatContext = prevContext;
1391 }
1392
1393 const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]];
@@ -1404,11 +1404,10 @@ function renderSuspenseBoundary(
1404 newBoundary.fallbackState,
1405 fallbackAbortSet,
1406 fallbackKeyPath,
1407 - task.formatContext,
1407 + getSuspenseFallbackFormatContext(task.formatContext),
1408 task.context,
1409 task.treeContext,
1410 task.componentStack,
1411 - true,
1411 !disableLegacyContext ? task.legacyContext : emptyContextObject,
1412 __DEV__ ? task.debugTask : null,
1413 );
@@ -1431,6 +1430,7 @@ function replaySuspenseBoundary(
1430 fallbackSlots: ResumeSlots,
1431 ): void {
1432 const prevKeyPath = task.keyPath;
1433 + const prevContext = task.formatContext;
1434 const previousReplaySet: ReplaySet = task.replay;
1435
1436 const parentBoundary = task.blockedBoundary;
@@ -1466,6 +1466,7 @@ function replaySuspenseBoundary(
1466 task.blockedBoundary = resumedBoundary;
1467 task.hoistableState = resumedBoundary.contentState;
1468 task.keyPath = keyPath;
1469 + task.formatContext = getSuspenseContentFormatContext(prevContext);
1470 task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1};
1471
1472 try {
@@ -1541,6 +1542,7 @@ function replaySuspenseBoundary(
1542 task.hoistableState = parentHoistableState;
1543 task.replay = previousReplaySet;
1544 task.keyPath = prevKeyPath;
1545 + task.formatContext = prevContext;
1546 }
1547
1548 const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]];
@@ -1562,11 +1564,10 @@ function replaySuspenseBoundary(
1564 resumedBoundary.fallbackState,
1565 fallbackAbortSet,
1566 fallbackKeyPath,
1565 - task.formatContext,
1567 + getSuspenseFallbackFormatContext(task.formatContext),
1568 task.context,
1569 task.treeContext,
1570 task.componentStack,
1569 - true,
1571 !disableLegacyContext ? task.legacyContext : emptyContextObject,
1572 __DEV__ ? task.debugTask : null,
1573 );
@@ -1608,7 +1609,6 @@ function renderPreamble(
1609 task.context,
1610 task.treeContext,
1611 task.componentStack,
1611 - task.isFallback,
1612 !disableLegacyContext ? task.legacyContext : emptyContextObject,
1613 __DEV__ ? task.debugTask : null,
1614 );
@@ -1653,7 +1653,6 @@ function renderHostElement(
1653 task.hoistableState,
1654 task.formatContext,
1655 segment.lastPushedText,
1656 - task.isFallback,
1656 );
1657 segment.lastPushedText = false;
1658 const prevContext = task.formatContext;
@@ -3495,7 +3494,6 @@ function spawnNewSuspendedReplayTask(
3494 task.context,
3495 task.treeContext,
3496 task.componentStack,
3498 - task.isFallback,
3497 !disableLegacyContext ? task.legacyContext : emptyContextObject,
3498 __DEV__ ? task.debugTask : null,
3499 );
@@ -3537,7 +3535,6 @@ function spawnNewSuspendedRenderTask(
3535 task.context,
3536 task.treeContext,
3537 task.componentStack,
3540 - task.isFallback,
3538 !disableLegacyContext ? task.legacyContext : emptyContextObject,
3539 __DEV__ ? task.debugTask : null,
3540 );
packages/react-server/src/forks/ReactFizzConfig.custom.js
+4
@@ -48,6 +48,10 @@ export const bindToConsole = $$$config.bindToConsole;
48 export const resetResumableState = $$$config.resetResumableState;
49 export const completeResumableState = $$$config.completeResumableState;
50 export const getChildFormatContext = $$$config.getChildFormatContext;
51 +export const getSuspenseFallbackFormatContext =
52 + $$$config.getSuspenseFallbackFormatContext;
53 +export const getSuspenseContentFormatContext =
54 + $$$config.getSuspenseContentFormatContext;
55 export const makeId = $$$config.makeId;
56 export const pushTextInstance = $$$config.pushTextInstance;
57 export const pushStartInstance = $$$config.pushStartInstance;