Skip to content

Commit

Permalink
[Fizz] Track postponed holes in the prerender pass (#27317)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
sebmarkbage committed Aug 31, 2023
1 parent 469fd47 commit 7aeecfd
Show file tree
Hide file tree
Showing 7 changed files with 383 additions and 263 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2fba484cd095ea79b940364cea5107fa4ca9f0c8
b70a0d70224ceb4e277bd8ac535a2caafa5c075a
163 changes: 96 additions & 67 deletions compiled/facebook-www/ReactDOMServer-dev.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ if (__DEV__) {
var React = require("react");
var ReactDOM = require("react-dom");

var ReactVersion = "18.3.0-www-classic-b66d17d9";
var ReactVersion = "18.3.0-www-classic-b794beee";

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

var PENDING = 0;
var COMPLETED = 1;
var FLUSHED = 2;
var ABORTED = 3;
var ERRORED = 4;
var POSTPONED = 5;
var OPEN = 0;
var CLOSING = 1;
var CLOSED = 2; // This is a default heuristic for how to split up the HTML content into progressive
Expand Down Expand Up @@ -9846,6 +9846,7 @@ function createRequest(
flushScheduled: false,
resumableState: resumableState,
renderState: renderState,
rootFormatContext: rootFormatContext,
progressiveChunkSize:
progressiveChunkSize === undefined
? DEFAULT_PROGRESSIVE_CHUNK_SIZE
Expand All @@ -9861,6 +9862,7 @@ function createRequest(
clientRenderedBoundaries: [],
completedBoundaries: [],
partialBoundaries: [],
trackedPostpones: null,
onError: onError === undefined ? defaultErrorHandler : onError,
onPostpone: onPostpone === undefined ? noop : onPostpone,
onAllReady: onAllReady === undefined ? noop : onAllReady,
Expand Down Expand Up @@ -9913,18 +9915,19 @@ function pingTask(request, task) {
}
}

function createSuspenseBoundary(request, fallbackAbortableTasks) {
function createSuspenseBoundary(request, fallbackAbortableTasks, keyPath) {
return {
status: PENDING,
id: UNINITIALIZED_SUSPENSE_BOUNDARY_ID,
rootSegmentID: -1,
parentFlushed: false,
pendingTasks: 0,
forceClientRender: false,
completedSegments: [],
byteSize: 0,
fallbackAbortableTasks: fallbackAbortableTasks,
errorDigest: null,
resources: createBoundaryResources()
resources: createBoundaryResources(),
keyPath: keyPath
};
}

Expand Down Expand Up @@ -10117,7 +10120,11 @@ function renderSuspenseBoundary(request, task, props) {
var fallback = props.fallback;
var content = props.children;
var fallbackAbortSet = new Set();
var newBoundary = createSuspenseBoundary(request, fallbackAbortSet);
var newBoundary = createSuspenseBoundary(
request,
fallbackAbortSet,
task.keyPath
);
var insertionIndex = parentSegment.chunks.length; // The children of the boundary segment is actually the fallback.

var boundarySegment = createPendingSegment(
Expand Down Expand Up @@ -10172,16 +10179,17 @@ function renderSuspenseBoundary(request, task, props) {
contentRootSegment.status = COMPLETED;
queueCompletedSegment(newBoundary, contentRootSegment);

if (newBoundary.pendingTasks === 0) {
// This must have been the last segment we were waiting on. This boundary is now complete.
if (newBoundary.pendingTasks === 0 && newBoundary.status === PENDING) {
newBoundary.status = COMPLETED; // This must have been the last segment we were waiting on. This boundary is now complete.
// Therefore we won't need the fallback. We early return so that we don't have to create
// the fallback.

popComponentStackInDEV(task);
return;
}
} catch (error) {
contentRootSegment.status = ERRORED;
newBoundary.forceClientRender = true;
newBoundary.status = CLIENT_RENDERED;
var errorDigest;

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

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

task.blockedSegment.formatContext = previousFormatContext;
task.legacyContext = previousLegacyContext;
task.context = previousContext;
task.keyPath = previousKeyPath; // Restore all active ReactContexts to what they were before.
task.blockedSegment.formatContext = previousFormatContext;
task.legacyContext = previousLegacyContext;
task.context = previousContext;
task.keyPath = previousKeyPath; // Restore all active ReactContexts to what they were before.

switchContext(previousContext);
switchContext(previousContext);

{
task.componentStack = previousComponentStack;
{
task.componentStack = previousComponentStack;
}

return;
}
} // Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.

return;
} else {
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
task.blockedSegment.formatContext = previousFormatContext;
task.legacyContext = previousLegacyContext;
task.context = previousContext;
task.keyPath = previousKeyPath; // Restore all active ReactContexts to what they were before.
task.blockedSegment.formatContext = previousFormatContext;
task.legacyContext = previousLegacyContext;
task.context = previousContext;
task.keyPath = previousKeyPath; // Restore all active ReactContexts to what they were before.

switchContext(previousContext);
switchContext(previousContext);

{
task.componentStack = previousComponentStack;
} // We assume that we don't need the correct context.
// Let's terminate the rest of the tree and don't render any siblings.
{
task.componentStack = previousComponentStack;
} // We assume that we don't need the correct context.
// Let's terminate the rest of the tree and don't render any siblings.

throw x;
}
throw x;
}
}

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

if (!boundary.forceClientRender) {
boundary.forceClientRender = true;
if (boundary.status !== CLIENT_RENDERED) {
boundary.status = CLIENT_RENDERED;
boundary.errorDigest = errorDigest;

{
Expand Down Expand Up @@ -11243,8 +11253,8 @@ function abortTask(task, request, error) {
} else {
boundary.pendingTasks--;

if (!boundary.forceClientRender) {
boundary.forceClientRender = true;
if (boundary.status !== CLIENT_RENDERED) {
boundary.status = CLIENT_RENDERED;
boundary.errorDigest = request.onError(error);

{
Expand Down Expand Up @@ -11331,9 +11341,12 @@ function finishedTask(request, boundary, segment) {
} else {
boundary.pendingTasks--;

if (boundary.forceClientRender);
if (boundary.status === CLIENT_RENDERED);
else if (boundary.pendingTasks === 0) {
// This must have been the last segment we were waiting on. This boundary is now complete.
if (boundary.status === PENDING) {
boundary.status = COMPLETED;
} // This must have been the last segment we were waiting on. This boundary is now complete.

if (segment.parentFlushed) {
// Our parent segment already flushed, so we need to schedule this segment to be emitted.
// If it is a segment that was aborted, we'll write other content instead so we don't need
Expand Down Expand Up @@ -11444,18 +11457,23 @@ function retryTask(request, task) {
// (unstable) API for suspending. This implementation detail can change
// later, once we deprecate the old API in favor of `use`.
getSuspendedThenable()
: thrownValue; // $FlowFixMe[method-unbinding]

if (typeof x === "object" && x !== null && typeof x.then === "function") {
// Something suspended again, let's pick it back up later.
var ping = task.ping;
x.then(ping, ping);
task.thenableState = getThenableStateAfterSuspending();
} else {
task.abortSet.delete(task);
segment.status = ERRORED;
erroredTask(request, task.blockedBoundary, segment, x);
: thrownValue;

if (typeof x === "object" && x !== null) {
// $FlowFixMe[method-unbinding]
if (typeof x.then === "function") {
// Something suspended again, let's pick it back up later.
var ping = task.ping;
x.then(ping, ping);
task.thenableState = getThenableStateAfterSuspending();
return;
}
}

task.abortSet.delete(task);
segment.status = ERRORED;
erroredTask(request, task.blockedBoundary, segment, x);
return;
} finally {
{
setCurrentlyRenderingBoundaryResourcesTarget(request.renderState, null);
Expand Down Expand Up @@ -11545,7 +11563,11 @@ function flushSubtree(request, destination, segment) {
case PENDING: {
// We're emitting a placeholder for this segment to be filled in later.
// Therefore we'll need to assign it an ID - to refer to it by.
var segmentID = (segment.id = request.nextSegmentId++); // When this segment finally completes it won't be embedded in text since it will flush separately
segment.id = request.nextSegmentId++; // Fallthrough
}

case POSTPONED: {
var segmentID = segment.id; // When this segment finally completes it won't be embedded in text since it will flush separately

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

if (boundary.forceClientRender) {
if (boundary.status === CLIENT_RENDERED) {
// Emit a client rendered suspense boundary wrapper.
// We never queue the inner boundary so we'll never emit its content or partial segments.
writeStartClientRenderedSuspenseBoundary(
Expand All @@ -11615,20 +11637,23 @@ function flushSegment(request, destination, segment) {
destination,
request.renderState
);
} else if (boundary.pendingTasks > 0) {
// This boundary is still loading. Emit a pending suspense boundary wrapper.
} else if (boundary.status !== COMPLETED) {
if (boundary.status === PENDING) {
boundary.id = assignSuspenseBoundaryID(
request.renderState,
request.resumableState
);
} // This boundary is still loading. Emit a pending suspense boundary wrapper.
// Assign an ID to refer to the future content by.

boundary.rootSegmentID = request.nextSegmentId++;

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

var id = (boundary.id = assignSuspenseBoundaryID(
request.renderState,
request.resumableState
));
var id = boundary.id;
writeStartPendingSuspenseBoundary(destination, request.renderState, id); // Flush the fallback.

flushSubtree(request, destination, segment);
Expand Down Expand Up @@ -11924,7 +11949,11 @@ function flushCompletedQueues(request, destination) {
request.flushScheduled = false;

{
writePostamble(destination, request.resumableState);
// We write the trailing tags but only if don't have any data to resume.
// If we need to resume we'll write the postamble in the resume instead.
{
writePostamble(destination, request.resumableState);
}
}

{
Expand All @@ -11940,7 +11969,7 @@ function flushCompletedQueues(request, destination) {
}
}

function startWork(request) {
function startRender(request) {
request.flushScheduled = request.destination !== null;

{
Expand Down Expand Up @@ -12019,7 +12048,7 @@ function flushResources(request) {
}
function getResumableState(request) {
return request.resumableState;
} // Returns the state of a postponed request or null if nothing was postponed.
}

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

abort(request, abortReason);
Expand Down
Loading

0 comments on commit 7aeecfd

Please sign in to comment.