Skip to content

Commit 7aeecfd

Browse files
committed
[Fizz] Track postponed holes in the prerender pass (#27317)
This is basically the implementation for the prerender pass. Instead of forking basically the whole implementation for prerender, I just add a conditional field on the request. If it's `null` it behaves like before. If it's non-`null` then instead of triggering client rendered boundaries it triggers those into a "postponed" state which is basically just a variant of "pending". It's supposed to be filled in later. It also builds up a serializable tree of which path can be followed to find the holes. This is basically a reverse `KeyPath` tree. It is unfortunate that this approach adds more code to the regular Fizz builds but in practice. It seems like this side is not going to add much code and we might instead just want to merge the builds so that it's smaller when you have `prerender` and `resume` in the same bundle - which I think will be common in practice. This just implements the prerender side, and not the resume side, which is why the tests have a TODO. That's in a follow up PR. DiffTrain build for [b70a0d7](b70a0d7)
1 parent 469fd47 commit 7aeecfd

7 files changed

+383
-263
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2fba484cd095ea79b940364cea5107fa4ca9f0c8
1+
b70a0d70224ceb4e277bd8ac535a2caafa5c075a

compiled/facebook-www/ReactDOMServer-dev.classic.js

Lines changed: 96 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ if (__DEV__) {
1919
var React = require("react");
2020
var ReactDOM = require("react-dom");
2121

22-
var ReactVersion = "18.3.0-www-classic-b66d17d9";
22+
var ReactVersion = "18.3.0-www-classic-b794beee";
2323

2424
// This refers to a WWW module.
2525
var warningWWW = require("warning");
@@ -9789,14 +9789,14 @@ function getStackByComponentStackNode(componentStack) {
97899789
var ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
97909790
var ReactCurrentCache = ReactSharedInternals.ReactCurrentCache;
97919791
var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame; // Linked list representing the identity of a component given the component/tag name and key.
9792-
// The name might be minified but we assume that it's going to be the same generated name. Typically
9793-
// because it's just the same compiled output in practice.
9792+
var CLIENT_RENDERED = 4; // if it errors or infinitely suspends
97949793

97959794
var PENDING = 0;
97969795
var COMPLETED = 1;
97979796
var FLUSHED = 2;
97989797
var ABORTED = 3;
97999798
var ERRORED = 4;
9799+
var POSTPONED = 5;
98009800
var OPEN = 0;
98019801
var CLOSING = 1;
98029802
var CLOSED = 2; // This is a default heuristic for how to split up the HTML content into progressive
@@ -9846,6 +9846,7 @@ function createRequest(
98469846
flushScheduled: false,
98479847
resumableState: resumableState,
98489848
renderState: renderState,
9849+
rootFormatContext: rootFormatContext,
98499850
progressiveChunkSize:
98509851
progressiveChunkSize === undefined
98519852
? DEFAULT_PROGRESSIVE_CHUNK_SIZE
@@ -9861,6 +9862,7 @@ function createRequest(
98619862
clientRenderedBoundaries: [],
98629863
completedBoundaries: [],
98639864
partialBoundaries: [],
9865+
trackedPostpones: null,
98649866
onError: onError === undefined ? defaultErrorHandler : onError,
98659867
onPostpone: onPostpone === undefined ? noop : onPostpone,
98669868
onAllReady: onAllReady === undefined ? noop : onAllReady,
@@ -9913,18 +9915,19 @@ function pingTask(request, task) {
99139915
}
99149916
}
99159917

9916-
function createSuspenseBoundary(request, fallbackAbortableTasks) {
9918+
function createSuspenseBoundary(request, fallbackAbortableTasks, keyPath) {
99179919
return {
9920+
status: PENDING,
99189921
id: UNINITIALIZED_SUSPENSE_BOUNDARY_ID,
99199922
rootSegmentID: -1,
99209923
parentFlushed: false,
99219924
pendingTasks: 0,
9922-
forceClientRender: false,
99239925
completedSegments: [],
99249926
byteSize: 0,
99259927
fallbackAbortableTasks: fallbackAbortableTasks,
99269928
errorDigest: null,
9927-
resources: createBoundaryResources()
9929+
resources: createBoundaryResources(),
9930+
keyPath: keyPath
99289931
};
99299932
}
99309933

@@ -10117,7 +10120,11 @@ function renderSuspenseBoundary(request, task, props) {
1011710120
var fallback = props.fallback;
1011810121
var content = props.children;
1011910122
var fallbackAbortSet = new Set();
10120-
var newBoundary = createSuspenseBoundary(request, fallbackAbortSet);
10123+
var newBoundary = createSuspenseBoundary(
10124+
request,
10125+
fallbackAbortSet,
10126+
task.keyPath
10127+
);
1012110128
var insertionIndex = parentSegment.chunks.length; // The children of the boundary segment is actually the fallback.
1012210129

1012310130
var boundarySegment = createPendingSegment(
@@ -10172,16 +10179,17 @@ function renderSuspenseBoundary(request, task, props) {
1017210179
contentRootSegment.status = COMPLETED;
1017310180
queueCompletedSegment(newBoundary, contentRootSegment);
1017410181

10175-
if (newBoundary.pendingTasks === 0) {
10176-
// This must have been the last segment we were waiting on. This boundary is now complete.
10182+
if (newBoundary.pendingTasks === 0 && newBoundary.status === PENDING) {
10183+
newBoundary.status = COMPLETED; // This must have been the last segment we were waiting on. This boundary is now complete.
1017710184
// Therefore we won't need the fallback. We early return so that we don't have to create
1017810185
// the fallback.
10186+
1017910187
popComponentStackInDEV(task);
1018010188
return;
1018110189
}
1018210190
} catch (error) {
1018310191
contentRootSegment.status = ERRORED;
10184-
newBoundary.forceClientRender = true;
10192+
newBoundary.status = CLIENT_RENDERED;
1018510193
var errorDigest;
1018610194

1018710195
{
@@ -11134,43 +11142,45 @@ function renderNode(request, task, node, childIndex) {
1113411142
// (unstable) API for suspending. This implementation detail can change
1113511143
// later, once we deprecate the old API in favor of `use`.
1113611144
getSuspendedThenable()
11137-
: thrownValue; // $FlowFixMe[method-unbinding]
11145+
: thrownValue;
1113811146

11139-
if (typeof x === "object" && x !== null && typeof x.then === "function") {
11140-
var wakeable = x;
11141-
var thenableState = getThenableStateAfterSuspending();
11142-
spawnNewSuspendedTask(request, task, thenableState, wakeable); // Restore the context. We assume that this will be restored by the inner
11143-
// functions in case nothing throws so we don't use "finally" here.
11147+
if (typeof x === "object" && x !== null) {
11148+
// $FlowFixMe[method-unbinding]
11149+
if (typeof x.then === "function") {
11150+
var wakeable = x;
11151+
var thenableState = getThenableStateAfterSuspending();
11152+
spawnNewSuspendedTask(request, task, thenableState, wakeable); // Restore the context. We assume that this will be restored by the inner
11153+
// functions in case nothing throws so we don't use "finally" here.
1114411154

11145-
task.blockedSegment.formatContext = previousFormatContext;
11146-
task.legacyContext = previousLegacyContext;
11147-
task.context = previousContext;
11148-
task.keyPath = previousKeyPath; // Restore all active ReactContexts to what they were before.
11155+
task.blockedSegment.formatContext = previousFormatContext;
11156+
task.legacyContext = previousLegacyContext;
11157+
task.context = previousContext;
11158+
task.keyPath = previousKeyPath; // Restore all active ReactContexts to what they were before.
1114911159

11150-
switchContext(previousContext);
11160+
switchContext(previousContext);
1115111161

11152-
{
11153-
task.componentStack = previousComponentStack;
11162+
{
11163+
task.componentStack = previousComponentStack;
11164+
}
11165+
11166+
return;
1115411167
}
11168+
} // Restore the context. We assume that this will be restored by the inner
11169+
// functions in case nothing throws so we don't use "finally" here.
1115511170

11156-
return;
11157-
} else {
11158-
// Restore the context. We assume that this will be restored by the inner
11159-
// functions in case nothing throws so we don't use "finally" here.
11160-
task.blockedSegment.formatContext = previousFormatContext;
11161-
task.legacyContext = previousLegacyContext;
11162-
task.context = previousContext;
11163-
task.keyPath = previousKeyPath; // Restore all active ReactContexts to what they were before.
11171+
task.blockedSegment.formatContext = previousFormatContext;
11172+
task.legacyContext = previousLegacyContext;
11173+
task.context = previousContext;
11174+
task.keyPath = previousKeyPath; // Restore all active ReactContexts to what they were before.
1116411175

11165-
switchContext(previousContext);
11176+
switchContext(previousContext);
1116611177

11167-
{
11168-
task.componentStack = previousComponentStack;
11169-
} // We assume that we don't need the correct context.
11170-
// Let's terminate the rest of the tree and don't render any siblings.
11178+
{
11179+
task.componentStack = previousComponentStack;
11180+
} // We assume that we don't need the correct context.
11181+
// Let's terminate the rest of the tree and don't render any siblings.
1117111182

11172-
throw x;
11173-
}
11183+
throw x;
1117411184
}
1117511185
}
1117611186

@@ -11187,8 +11197,8 @@ function erroredTask(request, boundary, segment, error) {
1118711197
} else {
1118811198
boundary.pendingTasks--;
1118911199

11190-
if (!boundary.forceClientRender) {
11191-
boundary.forceClientRender = true;
11200+
if (boundary.status !== CLIENT_RENDERED) {
11201+
boundary.status = CLIENT_RENDERED;
1119211202
boundary.errorDigest = errorDigest;
1119311203

1119411204
{
@@ -11243,8 +11253,8 @@ function abortTask(task, request, error) {
1124311253
} else {
1124411254
boundary.pendingTasks--;
1124511255

11246-
if (!boundary.forceClientRender) {
11247-
boundary.forceClientRender = true;
11256+
if (boundary.status !== CLIENT_RENDERED) {
11257+
boundary.status = CLIENT_RENDERED;
1124811258
boundary.errorDigest = request.onError(error);
1124911259

1125011260
{
@@ -11331,9 +11341,12 @@ function finishedTask(request, boundary, segment) {
1133111341
} else {
1133211342
boundary.pendingTasks--;
1133311343

11334-
if (boundary.forceClientRender);
11344+
if (boundary.status === CLIENT_RENDERED);
1133511345
else if (boundary.pendingTasks === 0) {
11336-
// This must have been the last segment we were waiting on. This boundary is now complete.
11346+
if (boundary.status === PENDING) {
11347+
boundary.status = COMPLETED;
11348+
} // This must have been the last segment we were waiting on. This boundary is now complete.
11349+
1133711350
if (segment.parentFlushed) {
1133811351
// Our parent segment already flushed, so we need to schedule this segment to be emitted.
1133911352
// If it is a segment that was aborted, we'll write other content instead so we don't need
@@ -11444,18 +11457,23 @@ function retryTask(request, task) {
1144411457
// (unstable) API for suspending. This implementation detail can change
1144511458
// later, once we deprecate the old API in favor of `use`.
1144611459
getSuspendedThenable()
11447-
: thrownValue; // $FlowFixMe[method-unbinding]
11448-
11449-
if (typeof x === "object" && x !== null && typeof x.then === "function") {
11450-
// Something suspended again, let's pick it back up later.
11451-
var ping = task.ping;
11452-
x.then(ping, ping);
11453-
task.thenableState = getThenableStateAfterSuspending();
11454-
} else {
11455-
task.abortSet.delete(task);
11456-
segment.status = ERRORED;
11457-
erroredTask(request, task.blockedBoundary, segment, x);
11460+
: thrownValue;
11461+
11462+
if (typeof x === "object" && x !== null) {
11463+
// $FlowFixMe[method-unbinding]
11464+
if (typeof x.then === "function") {
11465+
// Something suspended again, let's pick it back up later.
11466+
var ping = task.ping;
11467+
x.then(ping, ping);
11468+
task.thenableState = getThenableStateAfterSuspending();
11469+
return;
11470+
}
1145811471
}
11472+
11473+
task.abortSet.delete(task);
11474+
segment.status = ERRORED;
11475+
erroredTask(request, task.blockedBoundary, segment, x);
11476+
return;
1145911477
} finally {
1146011478
{
1146111479
setCurrentlyRenderingBoundaryResourcesTarget(request.renderState, null);
@@ -11545,7 +11563,11 @@ function flushSubtree(request, destination, segment) {
1154511563
case PENDING: {
1154611564
// We're emitting a placeholder for this segment to be filled in later.
1154711565
// Therefore we'll need to assign it an ID - to refer to it by.
11548-
var segmentID = (segment.id = request.nextSegmentId++); // When this segment finally completes it won't be embedded in text since it will flush separately
11566+
segment.id = request.nextSegmentId++; // Fallthrough
11567+
}
11568+
11569+
case POSTPONED: {
11570+
var segmentID = segment.id; // When this segment finally completes it won't be embedded in text since it will flush separately
1154911571

1155011572
segment.lastPushedText = false;
1155111573
segment.textEmbedded = false;
@@ -11599,7 +11621,7 @@ function flushSegment(request, destination, segment) {
1159911621
boundary.parentFlushed = true; // This segment is a Suspense boundary. We need to decide whether to
1160011622
// emit the content or the fallback now.
1160111623

11602-
if (boundary.forceClientRender) {
11624+
if (boundary.status === CLIENT_RENDERED) {
1160311625
// Emit a client rendered suspense boundary wrapper.
1160411626
// We never queue the inner boundary so we'll never emit its content or partial segments.
1160511627
writeStartClientRenderedSuspenseBoundary(
@@ -11615,20 +11637,23 @@ function flushSegment(request, destination, segment) {
1161511637
destination,
1161611638
request.renderState
1161711639
);
11618-
} else if (boundary.pendingTasks > 0) {
11619-
// This boundary is still loading. Emit a pending suspense boundary wrapper.
11640+
} else if (boundary.status !== COMPLETED) {
11641+
if (boundary.status === PENDING) {
11642+
boundary.id = assignSuspenseBoundaryID(
11643+
request.renderState,
11644+
request.resumableState
11645+
);
11646+
} // This boundary is still loading. Emit a pending suspense boundary wrapper.
1162011647
// Assign an ID to refer to the future content by.
11648+
1162111649
boundary.rootSegmentID = request.nextSegmentId++;
1162211650

1162311651
if (boundary.completedSegments.length > 0) {
1162411652
// If this is at least partially complete, we can queue it to be partially emitted early.
1162511653
request.partialBoundaries.push(boundary);
1162611654
} /// This is the first time we should have referenced this ID.
1162711655

11628-
var id = (boundary.id = assignSuspenseBoundaryID(
11629-
request.renderState,
11630-
request.resumableState
11631-
));
11656+
var id = boundary.id;
1163211657
writeStartPendingSuspenseBoundary(destination, request.renderState, id); // Flush the fallback.
1163311658

1163411659
flushSubtree(request, destination, segment);
@@ -11924,7 +11949,11 @@ function flushCompletedQueues(request, destination) {
1192411949
request.flushScheduled = false;
1192511950

1192611951
{
11927-
writePostamble(destination, request.resumableState);
11952+
// We write the trailing tags but only if don't have any data to resume.
11953+
// If we need to resume we'll write the postamble in the resume instead.
11954+
{
11955+
writePostamble(destination, request.resumableState);
11956+
}
1192811957
}
1192911958

1193011959
{
@@ -11940,7 +11969,7 @@ function flushCompletedQueues(request, destination) {
1194011969
}
1194111970
}
1194211971

11943-
function startWork(request) {
11972+
function startRender(request) {
1194411973
request.flushScheduled = request.destination !== null;
1194511974

1194611975
{
@@ -12019,7 +12048,7 @@ function flushResources(request) {
1201912048
}
1202012049
function getResumableState(request) {
1202112050
return request.resumableState;
12022-
} // Returns the state of a postponed request or null if nothing was postponed.
12051+
}
1202312052

1202412053
function onError() {
1202512054
// Non-fatal errors are ignored.
@@ -12076,7 +12105,7 @@ function renderToStringImpl(
1207612105
undefined,
1207712106
undefined
1207812107
);
12079-
startWork(request); // If anything suspended and is still pending, we'll abort it before writing.
12108+
startRender(request); // If anything suspended and is still pending, we'll abort it before writing.
1208012109
// That way we write only client-rendered boundaries from the start.
1208112110

1208212111
abort(request, abortReason);

0 commit comments

Comments
 (0)