Skip to content

Commit 0251ed2

Browse files
committed
Updates the approach to emit hoistables eagerly but only if the hoistable is not in a fallback tree. Effectively it makes hoistable elements deep in any fallback noops. The logic here is that fallbacks aren't hydrated, they're always either replaced by the server or client rendered. In either case the primary content should be replacing the fallback hositables and since hoistable elements are really just metadata the imperative need to transiently render them is not as well justified. This avoids a host of complexities around deleting hoistables from fallbacks when the boundary reveals it's content.
Along with this change I made it so primary content hositables flush eagerly and do not wait for their containing boundary to complete first. This is a progmatic solution to the problem of prerendering. when prerendering we assume that all transent state is entirely flushed in the static prelude however by holding back hoistables until the boundary is complete this is violated. We would either need to conditionally emit hoistables for incomplete boundaries when prerending or we would need to serialize the state of the hoistables to recover them on the resuem path. The arguments for holding them back are that it aligns with client hoistable semantics better and if the boundary never hydrates the hoistables will get orphaned. Both of these problems are not insignificant but are also not necessarily blockers and so this approach attempts to balance complexity over impact by emitting them eagerly.
1 parent 852f521 commit 0251ed2

File tree

6 files changed

+128
-266
lines changed

6 files changed

+128
-266
lines changed

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

+71-138
Original file line numberDiff line numberDiff line change
@@ -2222,10 +2222,10 @@ function pushMeta(
22222222
target: Array<Chunk | PrecomputedChunk>,
22232223
props: Object,
22242224
renderState: RenderState,
2225-
hoistableState: null | HoistableState,
22262225
textEmbedded: boolean,
22272226
insertionMode: InsertionMode,
22282227
noscriptTagInScope: boolean,
2228+
isFallback: boolean,
22292229
): null {
22302230
if (enableFloat) {
22312231
if (
@@ -2241,31 +2241,26 @@ function pushMeta(
22412241
target.push(textSeparator);
22422242
}
22432243

2244-
if (typeof props.charSet === 'string') {
2245-
return pushSelfClosing(
2246-
hoistableState
2247-
? hoistableState.charsetChunks
2248-
: renderState.charsetChunks,
2249-
props,
2250-
'meta',
2251-
);
2244+
if (isFallback) {
2245+
// Hoistable Elements for fallbacks are simply omitted. we don't want to emit them early
2246+
// because they are likely superceded by primary content and we want to avoid needing to clean
2247+
// them up when the primary content is ready. They are never hydrated on the client anyway because
2248+
// boundaries in fallback are awaited or client render, in either case there is never hydration
2249+
return null;
2250+
} else if (typeof props.charSet === 'string') {
2251+
// "charset" Should really be config and not picked up from tags however since this is
2252+
// the only way to embed the tag today we flush it on a special queue on the Request so it
2253+
// can go before everything else. Like viewport this means that the tag will escape it's
2254+
// parent container.
2255+
return pushSelfClosing(renderState.charsetChunks, props, 'meta');
22522256
} else if (props.name === 'viewport') {
2253-
// "viewport" isn't related to preconnect but it has the right priority
2254-
return pushSelfClosing(
2255-
hoistableState
2256-
? hoistableState.viewportChunks
2257-
: renderState.viewportChunks,
2258-
props,
2259-
'meta',
2260-
);
2257+
// "viewport" is flushed on the Request so it can go earlier that Float resources that
2258+
// might be affected by it. This means it can escape the boundary it is rendered within.
2259+
// This is a pragmatic solution to viewport being incredibly sensitive to document order
2260+
// without requiring all hoistables to be flushed too early.
2261+
return pushSelfClosing(renderState.viewportChunks, props, 'meta');
22612262
} else {
2262-
return pushSelfClosing(
2263-
hoistableState
2264-
? hoistableState.hoistableChunks
2265-
: renderState.hoistableChunks,
2266-
props,
2267-
'meta',
2268-
);
2263+
return pushSelfClosing(renderState.hoistableChunks, props, 'meta');
22692264
}
22702265
}
22712266
} else {
@@ -2282,6 +2277,7 @@ function pushLink(
22822277
textEmbedded: boolean,
22832278
insertionMode: InsertionMode,
22842279
noscriptTagInScope: boolean,
2280+
isFallback: boolean,
22852281
): null {
22862282
if (enableFloat) {
22872283
const rel = props.rel;
@@ -2437,10 +2433,15 @@ function pushLink(
24372433
target.push(textSeparator);
24382434
}
24392435

2440-
const hoistableChunks = hoistableState
2441-
? hoistableState.hoistableChunks
2442-
: renderState.hoistableChunks;
2443-
return pushLinkImpl(hoistableChunks, props);
2436+
if (isFallback) {
2437+
// Hoistable Elements for fallbacks are simply omitted. we don't want to emit them early
2438+
// because they are likely superceded by primary content and we want to avoid needing to clean
2439+
// them up when the primary content is ready. They are never hydrated on the client anyway because
2440+
// boundaries in fallback are awaited or client render, in either case there is never hydration
2441+
return null;
2442+
} else {
2443+
return pushLinkImpl(renderState.hoistableChunks, props);
2444+
}
24442445
}
24452446
} else {
24462447
return pushLinkImpl(target, props);
@@ -2894,9 +2895,9 @@ function pushTitle(
28942895
target: Array<Chunk | PrecomputedChunk>,
28952896
props: Object,
28962897
renderState: RenderState,
2897-
hoistableState: null | HoistableState,
28982898
insertionMode: InsertionMode,
28992899
noscriptTagInScope: boolean,
2900+
isFallback: boolean,
29002901
): ReactNodeList {
29012902
if (__DEV__) {
29022903
if (hasOwnProperty.call(props, 'children')) {
@@ -2952,11 +2953,15 @@ function pushTitle(
29522953
!noscriptTagInScope &&
29532954
props.itemProp == null
29542955
) {
2955-
const hoistableTarget = hoistableState
2956-
? hoistableState.hoistableChunks
2957-
: renderState.hoistableChunks;
2958-
pushTitleImpl(hoistableTarget, props);
2959-
return null;
2956+
if (isFallback) {
2957+
// Hoistable Elements for fallbacks are simply omitted. we don't want to emit them early
2958+
// because they are likely superceded by primary content and we want to avoid needing to clean
2959+
// them up when the primary content is ready. They are never hydrated on the client anyway because
2960+
// boundaries in fallback are awaited or client render, in either case there is never hydration
2961+
return null;
2962+
} else {
2963+
pushTitleImpl(renderState.hoistableChunks, props);
2964+
}
29602965
} else {
29612966
return pushTitleImpl(target, props);
29622967
}
@@ -3490,6 +3495,7 @@ export function pushStartInstance(
34903495
hoistableState: null | HoistableState,
34913496
formatContext: FormatContext,
34923497
textEmbedded: boolean,
3498+
isFallback: boolean,
34933499
): ReactNodeList {
34943500
if (__DEV__) {
34953501
validateARIAProperties(type, props);
@@ -3556,9 +3562,9 @@ export function pushStartInstance(
35563562
target,
35573563
props,
35583564
renderState,
3559-
hoistableState,
35603565
formatContext.insertionMode,
35613566
!!(formatContext.tagScope & NOSCRIPT_SCOPE),
3567+
isFallback,
35623568
)
35633569
: pushStartTitle(target, props);
35643570
case 'link':
@@ -3571,6 +3577,7 @@ export function pushStartInstance(
35713577
textEmbedded,
35723578
formatContext.insertionMode,
35733579
!!(formatContext.tagScope & NOSCRIPT_SCOPE),
3580+
isFallback,
35743581
);
35753582
case 'script':
35763583
return enableFloat
@@ -3600,10 +3607,10 @@ export function pushStartInstance(
36003607
target,
36013608
props,
36023609
renderState,
3603-
hoistableState,
36043610
textEmbedded,
36053611
formatContext.insertionMode,
36063612
!!(formatContext.tagScope & NOSCRIPT_SCOPE),
3613+
isFallback,
36073614
);
36083615
// Newline eating tags
36093616
case 'listing':
@@ -4469,7 +4476,7 @@ function hasStylesToHoist(stylesheet: StylesheetResource): boolean {
44694476
return false;
44704477
}
44714478

4472-
export function writeHoistablesForPartialBoundary(
4479+
export function writeHoistablesForBoundary(
44734480
destination: Destination,
44744481
hoistableState: HoistableState,
44754482
renderState: RenderState,
@@ -4495,57 +4502,6 @@ export function writeHoistablesForPartialBoundary(
44954502
return destinationHasCapacity;
44964503
}
44974504

4498-
export function writeHoistablesForCompletedBoundary(
4499-
destination: Destination,
4500-
hoistableState: HoistableState,
4501-
renderState: RenderState,
4502-
): boolean {
4503-
// Reset these on each invocation, they are only safe to read in this function
4504-
currentlyRenderingBoundaryHasStylesToHoist = false;
4505-
destinationHasCapacity = true;
4506-
4507-
// Flush style tags for each precedence this boundary depends on
4508-
hoistableState.styles.forEach(flushStyleTagsLateForBoundary, destination);
4509-
4510-
// Determine if this boundary has stylesheets that need to be awaited upon completion
4511-
hoistableState.stylesheets.forEach(hasStylesToHoist);
4512-
4513-
// Flush Hoistable Elements
4514-
let i;
4515-
const charsetChunks = hoistableState.charsetChunks;
4516-
for (i = 0; i < charsetChunks.length - 1; i++) {
4517-
writeChunk(destination, charsetChunks[i]);
4518-
}
4519-
if (i < charsetChunks.length) {
4520-
destinationHasCapacity = writeChunkAndReturn(destination, charsetChunks[i]);
4521-
}
4522-
const viewportChunks = hoistableState.viewportChunks;
4523-
for (i = 0; i < viewportChunks.length - 1; i++) {
4524-
writeChunk(destination, charsetChunks[i]);
4525-
}
4526-
if (i < viewportChunks.length) {
4527-
destinationHasCapacity = writeChunkAndReturn(
4528-
destination,
4529-
viewportChunks[i],
4530-
);
4531-
}
4532-
const hoistableChunks = hoistableState.hoistableChunks;
4533-
for (i = 0; i < hoistableChunks.length - 1; i++) {
4534-
writeChunk(destination, hoistableChunks[i]);
4535-
}
4536-
if (i < hoistableChunks.length) {
4537-
destinationHasCapacity = writeChunkAndReturn(
4538-
destination,
4539-
hoistableChunks[i],
4540-
);
4541-
}
4542-
4543-
if (currentlyRenderingBoundaryHasStylesToHoist) {
4544-
renderState.stylesToHoist = true;
4545-
}
4546-
return destinationHasCapacity;
4547-
}
4548-
45494505
function flushResource(this: Destination, resource: Resource) {
45504506
for (let i = 0; i < resource.length; i++) {
45514507
writeChunk(this, resource[i]);
@@ -4741,26 +4697,35 @@ export function writePreamble(
47414697
}
47424698
hoistableChunks.length = 0;
47434699

4744-
// Flush closing head if necessary
47454700
if (htmlChunks && headChunks === null) {
4746-
// We have an <html> rendered but no <head> rendered. We however inserted
4747-
// a <head> up above so we need to emit the </head> now. This is safe because
4748-
// if the main content contained the </head> it would also have provided a
4749-
// <head>. This means that all the content inside <html> is either <body> or
4750-
// invalid HTML
4701+
// we have an <html> but we inserted an implicit <head> tag. We need
4702+
// to close it since the main content won't have it
47514703
writeChunk(destination, endChunkForTag('head'));
47524704
}
47534705
}
47544706

4755-
// This is an opportunity to write hoistables however in the current implemention
4756-
// the only hoistables that make sense to write here are Resources. Hoistable Elements
4757-
// would have already been written as part of the preamble or will be written as part
4758-
// of a boundary completion and thus don't need to be written here.
4707+
// We don't bother reporting backpressure at the moment because we expect to
4708+
// flush the entire preamble in a single pass. This probably should be modified
4709+
// in the future to be backpressure sensitive but that requires a larger refactor
4710+
// of the flushing code in Fizz.
47594711
export function writeHoistables(
47604712
destination: Destination,
47614713
resumableState: ResumableState,
47624714
renderState: RenderState,
47634715
): void {
4716+
let i = 0;
4717+
4718+
// Emit high priority Hoistables
4719+
4720+
// We omit charsetChunks because we have already sent the shell and if it wasn't
4721+
// already sent it is too late now.
4722+
4723+
const viewportChunks = renderState.viewportChunks;
4724+
for (i = 0; i < viewportChunks.length; i++) {
4725+
writeChunk(destination, viewportChunks[i]);
4726+
}
4727+
viewportChunks.length = 0;
4728+
47644729
renderState.preconnects.forEach(flushResource, destination);
47654730
renderState.preconnects.clear();
47664731

@@ -4787,6 +4752,13 @@ export function writeHoistables(
47874752

47884753
renderState.bulkPreloads.forEach(flushResource, destination);
47894754
renderState.bulkPreloads.clear();
4755+
4756+
// Write embedding hoistableChunks
4757+
const hoistableChunks = renderState.hoistableChunks;
4758+
for (i = 0; i < hoistableChunks.length; i++) {
4759+
writeChunk(destination, hoistableChunks[i]);
4760+
}
4761+
hoistableChunks.length = 0;
47904762
}
47914763

47924764
export function writePostamble(
@@ -5259,10 +5231,6 @@ type StylesheetResource = {
52595231
export type HoistableState = {
52605232
styles: Set<StyleQueue>,
52615233
stylesheets: Set<StylesheetResource>,
5262-
// Hoistable chunks
5263-
charsetChunks: Array<Chunk | PrecomputedChunk>,
5264-
viewportChunks: Array<Chunk | PrecomputedChunk>,
5265-
hoistableChunks: Array<Chunk | PrecomputedChunk>,
52665234
};
52675235

52685236
export type StyleQueue = {
@@ -5276,9 +5244,6 @@ export function createHoistableState(): HoistableState {
52765244
return {
52775245
styles: new Set(),
52785246
stylesheets: new Set(),
5279-
charsetChunks: [],
5280-
viewportChunks: [],
5281-
hoistableChunks: [],
52825247
};
52835248
}
52845249

@@ -6142,44 +6107,12 @@ function hoistStylesheetDependency(
61426107
this.stylesheets.add(stylesheet);
61436108
}
61446109

6145-
export function hoistToBoundary(
6110+
export function hoistHoistables(
61466111
parentState: HoistableState,
61476112
childState: HoistableState,
61486113
): void {
61496114
childState.styles.forEach(hoistStyleQueueDependency, parentState);
61506115
childState.stylesheets.forEach(hoistStylesheetDependency, parentState);
6151-
let i;
6152-
const charsetChunks = childState.charsetChunks;
6153-
for (i = 0; i < charsetChunks.length; i++) {
6154-
parentState.charsetChunks.push(charsetChunks[i]);
6155-
}
6156-
const viewportChunks = childState.viewportChunks;
6157-
for (i = 0; i < charsetChunks.length; i++) {
6158-
parentState.viewportChunks.push(viewportChunks[i]);
6159-
}
6160-
const hoistableChunks = childState.hoistableChunks;
6161-
for (i = 0; i < hoistableChunks.length; i++) {
6162-
parentState.hoistableChunks.push(hoistableChunks[i]);
6163-
}
6164-
}
6165-
6166-
export function hoistToRoot(
6167-
renderState: RenderState,
6168-
hoistableState: HoistableState,
6169-
): void {
6170-
let i;
6171-
const charsetChunks = hoistableState.charsetChunks;
6172-
for (i = 0; i < charsetChunks.length; i++) {
6173-
renderState.charsetChunks.push(charsetChunks[i]);
6174-
}
6175-
const viewportChunks = hoistableState.viewportChunks;
6176-
for (i = 0; i < charsetChunks.length; i++) {
6177-
renderState.viewportChunks.push(viewportChunks[i]);
6178-
}
6179-
const hoistableChunks = hoistableState.hoistableChunks;
6180-
for (i = 0; i < hoistableChunks.length; i++) {
6181-
renderState.hoistableChunks.push(hoistableChunks[i]);
6182-
}
61836116
}
61846117

61856118
// This function is called at various times depending on whether we are rendering

packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,7 @@ export {
153153
writeClientRenderBoundaryInstruction,
154154
writeStartPendingSuspenseBoundary,
155155
writeEndPendingSuspenseBoundary,
156-
writeHoistablesForPartialBoundary,
157-
writeHoistablesForCompletedBoundary,
156+
writeHoistablesForBoundary,
158157
writePlaceholder,
159158
writeCompletedRoot,
160159
createRootFormatContext,
@@ -163,8 +162,7 @@ export {
163162
writePreamble,
164163
writeHoistables,
165164
writePostamble,
166-
hoistToBoundary,
167-
hoistToRoot,
165+
hoistHoistables,
168166
prepareHostDispatcher,
169167
resetResumableState,
170168
completeResumableState,

0 commit comments

Comments
 (0)