[Fizz] Unify preamble only fields to save a field (#35068)
Stacked on #35067. Same idea of saving a field on the SuspenseBoundary in the common case. The case where they can have a preamble is rare.
Sebastian Markbåge committed
Nov 7, 2025 at 09:19 UTC
fa50caf5f84bb8976aa1dbff7f15a821e44e7af7
1 file changed
+31
-29
packages/react-server/src/ReactFizzServer.js
+31
-29
@@ -255,8 +255,7 @@ type SuspenseBoundary = {
255
fallbackAbortableTasks: Set<Task>, // used to cancel task on the fallback if the boundary completes or gets canceled.
256
contentState: HoistableState,
257
fallbackState: HoistableState,
258
- contentPreamble: null | Preamble,
259
- fallbackPreamble: null | Preamble,
258
+ preamble: null | Preamble,
259
tracked: null | {
260
contentKeyPath: null | KeyNode, // used to track the path for replay nodes
261
fallbackNode: null | ReplayNode, // used to track the fallback for replay nodes
@@ -275,7 +274,7 @@ type RenderTask = {
274
ping: () => void,
275
blockedBoundary: Root | SuspenseBoundary,
276
blockedSegment: Segment, // the segment we'll write to
278
- blockedPreamble: null | Preamble,
277
+ blockedPreamble: null | PreambleState,
278
hoistableState: null | HoistableState, // Boundary state we'll mutate while rendering. This may not equal the state of the blockedBoundary
279
abortSet: Set<Task>, // the abortable set that this task belongs to
280
keyPath: Root | KeyNode, // the path of all parent keys currently rendering
@@ -398,7 +397,17 @@ export opaque type Request = {
397
didWarnForKey?: null | WeakSet<ComponentStackNode>,
398
};
399
401
-type Preamble = PreambleState;
400
+type Preamble = {
401
+ content: PreambleState,
402
+ fallback: PreambleState,
403
+};
404
+
405
+function createPreamble(): Preamble {
406
+ return {
407
+ content: createPreambleState(),
408
+ fallback: createPreambleState(),
409
+ };
410
+}
411
412
// This is a default heuristic for how to split up the HTML content into progressive
413
// loading. Our goal is to be able to display additional new content about every 500ms.
@@ -466,7 +475,7 @@ function isEligibleForOutlining(
475
// For boundaries that can possibly contribute to the preamble we don't want to outline
476
// them regardless of their size since the fallbacks should only be emitted if we've
477
// errored the boundary.
469
- boundary.contentPreamble === null
478
+ boundary.preamble === null
479
);
480
}
481
@@ -786,8 +795,7 @@ function createSuspenseBoundary(
795
request: Request,
796
row: null | SuspenseListRow,
797
fallbackAbortableTasks: Set<Task>,
789
- contentPreamble: null | Preamble,
790
- fallbackPreamble: null | Preamble,
798
+ preamble: null | Preamble,
799
defer: boolean,
800
): SuspenseBoundary {
801
const boundary: SuspenseBoundary = {
@@ -803,8 +811,7 @@ function createSuspenseBoundary(
811
errorDigest: null,
812
contentState: createHoistableState(),
813
fallbackState: createHoistableState(),
806
- contentPreamble,
807
- fallbackPreamble,
814
+ preamble,
815
tracked: null,
816
};
817
if (__DEV__) {
@@ -838,7 +845,7 @@ function createRenderTask(
845
childIndex: number,
846
blockedBoundary: Root | SuspenseBoundary,
847
blockedSegment: Segment,
841
- blockedPreamble: null | Preamble,
848
+ blockedPreamble: null | PreambleState,
849
hoistableState: null | HoistableState,
850
abortSet: Set<Task>,
851
keyPath: Root | KeyNode,
@@ -1290,8 +1297,7 @@ function renderSuspenseBoundary(
1297
request,
1298
task.row,
1299
fallbackAbortSet,
1293
- createPreambleState(),
1294
- createPreambleState(),
1300
+ createPreamble(),
1301
defer,
1302
);
1303
} else {
@@ -1300,7 +1306,6 @@ function renderSuspenseBoundary(
1306
task.row,
1307
fallbackAbortSet,
1308
null,
1303
- null,
1309
defer,
1310
);
1311
}
@@ -1365,7 +1370,8 @@ function renderSuspenseBoundary(
1370
}
1371
1372
task.blockedSegment = boundarySegment;
1368
- task.blockedPreamble = newBoundary.fallbackPreamble;
1373
+ task.blockedPreamble =
1374
+ newBoundary.preamble === null ? null : newBoundary.preamble.fallback;
1375
task.keyPath = fallbackKeyPath;
1376
task.formatContext = getSuspenseFallbackFormatContext(
1377
request.resumableState,
@@ -1409,7 +1415,7 @@ function renderSuspenseBoundary(
1415
-1,
1416
newBoundary,
1417
contentRootSegment,
1412
- newBoundary.contentPreamble,
1418
+ newBoundary.preamble === null ? null : newBoundary.preamble.content,
1419
newBoundary.contentState,
1420
task.abortSet,
1421
keyPath,
@@ -1440,7 +1446,8 @@ function renderSuspenseBoundary(
1446
// context switching. We just need to temporarily switch which boundary and which segment
1447
// we're writing to. If something suspends, it'll spawn new suspended task with that context.
1448
task.blockedBoundary = newBoundary;
1443
- task.blockedPreamble = newBoundary.contentPreamble;
1449
+ task.blockedPreamble =
1450
+ newBoundary.preamble === null ? null : newBoundary.preamble.content;
1451
task.hoistableState = newBoundary.contentState;
1452
task.blockedSegment = contentRootSegment;
1453
task.keyPath = keyPath;
@@ -1548,7 +1555,7 @@ function renderSuspenseBoundary(
1555
-1,
1556
parentBoundary,
1557
boundarySegment,
1551
- newBoundary.fallbackPreamble,
1558
+ newBoundary.preamble === null ? null : newBoundary.preamble.fallback,
1559
newBoundary.fallbackState,
1560
fallbackAbortSet,
1561
fallbackKeyPath,
@@ -1602,8 +1609,7 @@ function replaySuspenseBoundary(
1609
request,
1610
task.row,
1611
fallbackAbortSet,
1605
- createPreambleState(),
1606
- createPreambleState(),
1612
+ createPreamble(),
1613
defer,
1614
);
1615
} else {
@@ -1612,7 +1618,6 @@ function replaySuspenseBoundary(
1618
task.row,
1619
fallbackAbortSet,
1620
null,
1615
- null,
1621
defer,
1622
);
1623
}
@@ -4372,7 +4377,7 @@ function erroredTask(
4377
if (
4378
request.pendingRootTasks === 0 &&
4379
request.trackedPostpones === null &&
4375
- boundary.contentPreamble !== null
4380
+ boundary.preamble !== null
4381
) {
4382
// The root is complete and this boundary may contribute part of the preamble.
4383
// We eagerly attempt to prepare the preamble here because we expect most requests
@@ -4414,7 +4419,6 @@ function abortRemainingSuspenseBoundary(
4419
null,
4420
new Set(),
4421
null,
4417
- null,
4422
false,
4423
);
4424
resumedBoundary.parentFlushed = true;
@@ -4894,7 +4898,7 @@ function finishedTask(
4898
if (
4899
request.pendingRootTasks === 0 &&
4900
request.trackedPostpones === null &&
4897
- boundary.contentPreamble !== null
4901
+ boundary.preamble !== null
4902
) {
4903
// The root is complete and this boundary may contribute part of the preamble.
4904
// We eagerly attempt to prepare the preamble here because we expect most requests
@@ -5271,10 +5275,8 @@ function preparePreambleFromSegment(
5275
);
5276
}
5277
5274
- const preamble = boundary.contentPreamble;
5275
- const fallbackPreamble = boundary.fallbackPreamble;
5276
-
5277
- if (preamble === null || fallbackPreamble === null) {
5278
+ const preamble = boundary.preamble;
5279
+ if (preamble === null) {
5280
// This boundary cannot have a preamble so it can't block the flushing of
5281
// the preamble.
5282
return false;
@@ -5286,7 +5288,7 @@ function preparePreambleFromSegment(
5288
case COMPLETED: {
5289
// This boundary is complete. It might have inner boundaries which are pending
5290
// and able to provide a preamble so we have to check it's children
5289
- hoistPreambleState(request.renderState, preamble);
5291
+ hoistPreambleState(request.renderState, preamble.content);
5292
// We track this boundary's byteSize on the request since it will always flush with
5293
// the request since it may contribute to the preamble
5294
request.byteSize += boundary.byteSize;
@@ -5315,7 +5317,7 @@ function preparePreambleFromSegment(
5317
case CLIENT_RENDERED: {
5318
if (segment.status === COMPLETED) {
5319
// This boundary is errored so if it contains a preamble we should include it
5318
- hoistPreambleState(request.renderState, fallbackPreamble);
5320
+ hoistPreambleState(request.renderState, preamble.fallback);
5321
return preparePreambleFromSubtree(
5322
request,
5323
segment,