[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;