Emit Preamble Contribution inline instead of the end of a boundary (#32850)
This lets us write them early in the render phase. This should be safe because even if we write them deeply, then they still can't be wrapped by a element because then they'd no longer be in the document scope anymore. They end up flat in the body and so when we search the content we'll discover them.
Sebastian Markbåge committed
Apr 10, 2025 at 19:42 UTC
8a3c5e1a8d1d89a68ca36c6959c1f253710f6cef
6 files changed
+47
-123
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+20
-38
@@ -230,9 +230,9 @@ const SUSPENSE_START_DATA = '$';
230
const SUSPENSE_END_DATA = '/$';
231
const SUSPENSE_PENDING_START_DATA = '$?';
232
const SUSPENSE_FALLBACK_START_DATA = '$!';
233
-const PREAMBLE_CONTRIBUTION_HTML = 0b001;
234
-const PREAMBLE_CONTRIBUTION_BODY = 0b010;
235
-const PREAMBLE_CONTRIBUTION_HEAD = 0b100;
233
+const PREAMBLE_CONTRIBUTION_HTML = 'html';
234
+const PREAMBLE_CONTRIBUTION_BODY = 'body';
235
+const PREAMBLE_CONTRIBUTION_HEAD = 'head';
236
const FORM_STATE_IS_MATCHING = 'F!';
237
const FORM_STATE_IS_NOT_MATCHING = 'F';
238
@@ -1054,7 +1054,6 @@ export function clearSuspenseBoundary(
1054
suspenseInstance: SuspenseInstance,
1055
): void {
1056
let node: Node = suspenseInstance;
1057
- let possiblePreambleContribution: number = 0;
1057
// Delete all nodes within this suspense boundary.
1058
// There might be nested nodes so we need to keep track of how
1059
// deep we are and only break out when we're back on top.
@@ -1065,36 +1064,6 @@ export function clearSuspenseBoundary(
1064
if (nextNode && nextNode.nodeType === COMMENT_NODE) {
1065
const data = ((nextNode: any).data: string);
1066
if (data === SUSPENSE_END_DATA) {
1068
- if (
1069
- // represents 3 bits where at least one bit is set (1-7)
1070
- possiblePreambleContribution > 0 &&
1071
- possiblePreambleContribution < 8
1072
- ) {
1073
- const code = possiblePreambleContribution;
1074
- // It's not normally possible to insert a comment immediately preceding Suspense boundary
1075
- // closing comment marker so we can infer that if the comment preceding starts with "1" through "7"
1076
- // then it is in fact a preamble contribution marker comment. We do this value test to avoid the case
1077
- // where the Suspense boundary is empty and the preceding comment marker is the Suspense boundary
1078
- // opening marker or the closing marker of an inner boundary. In those cases the first character won't
1079
- // have the requisite value to be interpreted as a Preamble contribution
1080
- const ownerDocument = parentInstance.ownerDocument;
1081
- if (code & PREAMBLE_CONTRIBUTION_HTML) {
1082
- const documentElement: Element =
1083
- (ownerDocument.documentElement: any);
1084
- releaseSingletonInstance(documentElement);
1085
- }
1086
- if (code & PREAMBLE_CONTRIBUTION_BODY) {
1087
- const body: Element = (ownerDocument.body: any);
1088
- releaseSingletonInstance(body);
1089
- }
1090
- if (code & PREAMBLE_CONTRIBUTION_HEAD) {
1091
- const head: Element = (ownerDocument.head: any);
1092
- releaseSingletonInstance(head);
1093
- // We need to clear the head because this is the only singleton that can have children that
1094
- // were part of this boundary but are not inside this boundary.
1095
- clearHead(head);
1096
- }
1097
- }
1067
if (depth === 0) {
1068
parentInstance.removeChild(nextNode);
1069
// Retry if any event replaying was blocked on this.
@@ -1109,11 +1078,24 @@ export function clearSuspenseBoundary(
1078
data === SUSPENSE_FALLBACK_START_DATA
1079
) {
1080
depth++;
1112
- } else {
1113
- possiblePreambleContribution = data.charCodeAt(0) - 48;
1081
+ } else if (data === PREAMBLE_CONTRIBUTION_HTML) {
1082
+ // If a preamble contribution marker is found within the bounds of this boundary,
1083
+ // then it contributed to the html tag and we need to reset it.
1084
+ const ownerDocument = parentInstance.ownerDocument;
1085
+ const documentElement: Element = (ownerDocument.documentElement: any);
1086
+ releaseSingletonInstance(documentElement);
1087
+ } else if (data === PREAMBLE_CONTRIBUTION_HEAD) {
1088
+ const ownerDocument = parentInstance.ownerDocument;
1089
+ const head: Element = (ownerDocument.head: any);
1090
+ releaseSingletonInstance(head);
1091
+ // We need to clear the head because this is the only singleton that can have children that
1092
+ // were part of this boundary but are not inside this boundary.
1093
+ clearHead(head);
1094
+ } else if (data === PREAMBLE_CONTRIBUTION_BODY) {
1095
+ const ownerDocument = parentInstance.ownerDocument;
1096
+ const body: Element = (ownerDocument.body: any);
1097
+ releaseSingletonInstance(body);
1098
}
1115
- } else {
1116
- possiblePreambleContribution = 0;
1099
}
1100
// $FlowFixMe[incompatible-type] we bail out when we get a null
1101
node = nextNode;
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+22
-56
@@ -692,23 +692,16 @@ export function completeResumableState(resumableState: ResumableState): void {
692
resumableState.bootstrapModules = undefined;
693
}
694
695
-const NoContribution /* */ = 0b000;
696
-const HTMLContribution /* */ = 0b001;
697
-const BodyContribution /* */ = 0b010;
698
-const HeadContribution /* */ = 0b100;
699
-
695
export type PreambleState = {
696
htmlChunks: null | Array<Chunk | PrecomputedChunk>,
697
headChunks: null | Array<Chunk | PrecomputedChunk>,
698
bodyChunks: null | Array<Chunk | PrecomputedChunk>,
704
- contribution: number,
699
};
700
export function createPreambleState(): PreambleState {
701
return {
702
htmlChunks: null,
703
headChunks: null,
704
bodyChunks: null,
711
- contribution: NoContribution,
705
};
706
}
707
@@ -3279,6 +3272,12 @@ function pushTitleImpl(
3272
return null;
3273
}
3274
3275
+// These are used by the client if we clear a boundary and we find these, then we
3276
+// also clear the singleton as well.
3277
+const headPreambleContributionChunk = stringToPrecomputedChunk('<!--head-->');
3278
+const bodyPreambleContributionChunk = stringToPrecomputedChunk('<!--body-->');
3279
+const htmlPreambleContributionChunk = stringToPrecomputedChunk('<!--html-->');
3280
+
3281
function pushStartHead(
3282
target: Array<Chunk | PrecomputedChunk>,
3283
props: Object,
@@ -3293,6 +3292,12 @@ function pushStartHead(
3292
if (preamble.headChunks) {
3293
throw new Error(`The ${'`<head>`'} tag may only be rendered once.`);
3294
}
3295
+
3296
+ // Insert a marker in the body where the contribution to the head was in case we need to clear it.
3297
+ if (preambleState !== null) {
3298
+ target.push(headPreambleContributionChunk);
3299
+ }
3300
+
3301
preamble.headChunks = [];
3302
return pushStartSingletonElement(preamble.headChunks, props, 'head');
3303
} else {
@@ -3317,6 +3322,11 @@ function pushStartBody(
3322
throw new Error(`The ${'`<body>`'} tag may only be rendered once.`);
3323
}
3324
3325
+ // Insert a marker in the body where the contribution to the body tag was in case we need to clear it.
3326
+ if (preambleState !== null) {
3327
+ target.push(bodyPreambleContributionChunk);
3328
+ }
3329
+
3330
preamble.bodyChunks = [];
3331
return pushStartSingletonElement(preamble.bodyChunks, props, 'body');
3332
} else {
@@ -3341,6 +3351,11 @@ function pushStartHtml(
3351
throw new Error(`The ${'`<html>`'} tag may only be rendered once.`);
3352
}
3353
3354
+ // Insert a marker in the body where the contribution to the head was in case we need to clear it.
3355
+ if (preambleState !== null) {
3356
+ target.push(htmlPreambleContributionChunk);
3357
+ }
3358
+
3359
preamble.htmlChunks = [DOCTYPE];
3360
return pushStartSingletonElement(preamble.htmlChunks, props, 'html');
3361
} else {
@@ -4013,15 +4028,12 @@ export function hoistPreambleState(
4028
const rootPreamble = renderState.preamble;
4029
if (rootPreamble.htmlChunks === null && preambleState.htmlChunks) {
4030
rootPreamble.htmlChunks = preambleState.htmlChunks;
4016
- preambleState.contribution |= HTMLContribution;
4031
}
4032
if (rootPreamble.headChunks === null && preambleState.headChunks) {
4033
rootPreamble.headChunks = preambleState.headChunks;
4020
- preambleState.contribution |= HeadContribution;
4034
}
4035
if (rootPreamble.bodyChunks === null && preambleState.bodyChunks) {
4036
rootPreamble.bodyChunks = preambleState.bodyChunks;
4024
- preambleState.contribution |= BodyContribution;
4037
}
4038
}
4039
@@ -4101,11 +4113,7 @@ export function pushStartActivityBoundary(
4113
export function pushEndActivityBoundary(
4114
target: Array<Chunk | PrecomputedChunk>,
4115
renderState: RenderState,
4104
- preambleState: null | PreambleState,
4116
): void {
4106
- if (preambleState) {
4107
- pushPreambleContribution(target, preambleState);
4108
- }
4117
target.push(endActivityBoundary);
4118
}
4119
@@ -4220,11 +4228,7 @@ export function writeStartClientRenderedSuspenseBoundary(
4228
export function writeEndCompletedSuspenseBoundary(
4229
destination: Destination,
4230
renderState: RenderState,
4223
- preambleState: null | PreambleState,
4231
): boolean {
4225
- if (preambleState) {
4226
- writePreambleContribution(destination, preambleState);
4227
- }
4232
return writeChunkAndReturn(destination, endSuspenseBoundary);
4233
}
4234
export function writeEndPendingSuspenseBoundary(
@@ -4236,48 +4240,10 @@ export function writeEndPendingSuspenseBoundary(
4240
export function writeEndClientRenderedSuspenseBoundary(
4241
destination: Destination,
4242
renderState: RenderState,
4239
- preambleState: null | PreambleState,
4243
): boolean {
4241
- if (preambleState) {
4242
- writePreambleContribution(destination, preambleState);
4243
- }
4244
return writeChunkAndReturn(destination, endSuspenseBoundary);
4245
}
4246
4247
-const boundaryPreambleContributionChunkStart = stringToPrecomputedChunk('<!--');
4248
-const boundaryPreambleContributionChunkEnd = stringToPrecomputedChunk('-->');
4249
-
4250
-function pushPreambleContribution(
4251
- target: Array<Chunk | PrecomputedChunk>,
4252
- preambleState: PreambleState,
4253
-) {
4254
- // Same as writePreambleContribution but for the render phase.
4255
- const contribution = preambleState.contribution;
4256
- if (contribution !== NoContribution) {
4257
- target.push(
4258
- boundaryPreambleContributionChunkStart,
4259
- // This is a number type so we can do the fast path without coercion checking
4260
- // eslint-disable-next-line react-internal/safe-string-coercion
4261
- stringToChunk('' + contribution),
4262
- boundaryPreambleContributionChunkEnd,
4263
- );
4264
- }
4265
-}
4266
-
4267
-function writePreambleContribution(
4268
- destination: Destination,
4269
- preambleState: PreambleState,
4270
-) {
4271
- const contribution = preambleState.contribution;
4272
- if (contribution !== NoContribution) {
4273
- writeChunk(destination, boundaryPreambleContributionChunkStart);
4274
- // This is a number type so we can do the fast path without coercion checking
4275
- // eslint-disable-next-line react-internal/safe-string-coercion
4276
- writeChunk(destination, stringToChunk('' + contribution));
4277
- writeChunk(destination, boundaryPreambleContributionChunkEnd);
4278
- }
4279
-}
4280
-
4247
const startSegmentHTML = stringToPrecomputedChunk('<div hidden id="');
4248
const startSegmentHTML2 = stringToPrecomputedChunk('">');
4249
const endSegmentHTML = stringToPrecomputedChunk('</div>');
packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
+3
-14
@@ -224,12 +224,11 @@ export function pushStartActivityBoundary(
224
export function pushEndActivityBoundary(
225
target: Array<Chunk | PrecomputedChunk>,
226
renderState: RenderState,
227
- preambleState: null | PreambleState,
227
): void {
228
if (renderState.generateStaticMarkup) {
229
return;
230
}
232
- pushEndActivityBoundaryImpl(target, renderState, preambleState);
231
+ pushEndActivityBoundaryImpl(target, renderState);
232
}
233
234
export function writeStartCompletedSuspenseBoundary(
@@ -269,30 +268,20 @@ export function writeStartClientRenderedSuspenseBoundary(
268
export function writeEndCompletedSuspenseBoundary(
269
destination: Destination,
270
renderState: RenderState,
272
- preambleState: null | PreambleState,
271
): boolean {
272
if (renderState.generateStaticMarkup) {
273
return true;
274
}
277
- return writeEndCompletedSuspenseBoundaryImpl(
278
- destination,
279
- renderState,
280
- preambleState,
281
- );
275
+ return writeEndCompletedSuspenseBoundaryImpl(destination, renderState);
276
}
277
export function writeEndClientRenderedSuspenseBoundary(
278
destination: Destination,
279
renderState: RenderState,
286
- preambleState: null | PreambleState,
280
): boolean {
281
if (renderState.generateStaticMarkup) {
282
return true;
283
}
291
- return writeEndClientRenderedSuspenseBoundaryImpl(
292
- destination,
293
- renderState,
294
- preambleState,
295
- );
284
+ return writeEndClientRenderedSuspenseBoundaryImpl(destination, renderState);
285
}
286
287
export type TransitionStatus = FormStatus;
packages/react-markup/src/ReactFizzConfigMarkup.js
-3
@@ -162,7 +162,6 @@ export function pushStartActivityBoundary(
162
export function pushEndActivityBoundary(
163
target: Array<Chunk | PrecomputedChunk>,
164
renderState: RenderState,
165
- preambleState: null | PreambleState,
165
): void {
166
// Markup doesn't have any instructions.
167
return;
@@ -192,7 +191,6 @@ export function writeStartClientRenderedSuspenseBoundary(
191
export function writeEndCompletedSuspenseBoundary(
192
destination: Destination,
193
renderState: RenderState,
195
- preambleState: null | PreambleState,
194
): boolean {
195
// Markup doesn't have any instructions.
196
return true;
@@ -200,7 +198,6 @@ export function writeEndCompletedSuspenseBoundary(
198
export function writeEndClientRenderedSuspenseBoundary(
199
destination: Destination,
200
renderState: RenderState,
203
- preambleState: null | PreambleState,
201
): boolean {
202
// Markup doesn't have any instructions.
203
return true;
packages/react-noop-renderer/src/ReactNoopServer.js
-1
@@ -181,7 +181,6 @@ const ReactNoopServer = ReactFizzServer({
181
pushEndActivityBoundary(
182
target: Array<Uint8Array>,
183
renderState: RenderState,
184
- preambleState: null | PreambleState,
184
): void {
185
target.push(POP);
186
},
packages/react-server/src/ReactFizzServer.js
+2
-11
@@ -2243,11 +2243,7 @@ function renderActivity(
2243
renderNode(request, task, props.children, -1);
2244
task.keyPath = prevKeyPath;
2245
}
2246
- pushEndActivityBoundary(
2247
- segment.chunks,
2248
- request.renderState,
2249
- task.blockedPreamble,
2250
- );
2246
+ pushEndActivityBoundary(segment.chunks, request.renderState);
2247
segment.lastPushedText = false;
2248
}
2249
}
@@ -4908,7 +4904,6 @@ function flushSegment(
4904
return writeEndClientRenderedSuspenseBoundary(
4905
destination,
4906
request.renderState,
4911
- boundary.fallbackPreamble,
4907
);
4908
} else if (boundary.status !== COMPLETED) {
4909
if (boundary.status === PENDING) {
@@ -4979,11 +4974,7 @@ function flushSegment(
4974
const contentSegment = completedSegments[0];
4975
flushSegment(request, destination, contentSegment, hoistableState);
4976
4982
- return writeEndCompletedSuspenseBoundary(
4983
- destination,
4984
- request.renderState,
4985
- boundary.contentPreamble,
4986
- );
4977
+ return writeEndCompletedSuspenseBoundary(destination, request.renderState);
4978
}
4979
}
4980