Skip to content

Commit 7a4e14f

Browse files
committed
Split the rendering of a node from recursively rendering a node
This lets us reuse render node at the root which doesn't spawn new work.
1 parent dc43bbd commit 7a4e14f

File tree

1 file changed

+87
-49
lines changed

1 file changed

+87
-49
lines changed

packages/react-server/src/ReactFizzServer.js

Lines changed: 87 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ function renderSuspenseBoundary(
327327
task.blockedBoundary = newBoundary;
328328
task.blockedSegment = contentRootSegment;
329329
try {
330+
// We use the safe form because we don't handle suspending here. Only error handling.
330331
renderNode(request, task, content);
331332
contentRootSegment.status = COMPLETED;
332333
newBoundary.completedSegments.push(contentRootSegment);
@@ -382,13 +383,27 @@ function renderHostElement(
382383
task.assignID = null;
383384
const prevContext = segment.formatContext;
384385
segment.formatContext = getChildFormatContext(prevContext, type, props);
386+
// We use the non-destructive form because if something suspends, we still
387+
// need to pop back up and finish this subtree of HTML.
385388
renderNode(request, task, children);
386389
// We expect that errors will fatal the whole task and that we don't need
387390
// the correct context. Therefore this is not in a finally.
388391
segment.formatContext = prevContext;
389392
pushEndInstance(segment.chunks, type, props);
390393
}
391394

395+
function renderFunctionComponent(
396+
request: Request,
397+
task: Task,
398+
type: (props: any) => ReactNodeList,
399+
props: any,
400+
): void {
401+
const result = type(props);
402+
// We're now successfully past this task, and we don't have to pop back to
403+
// the previous task every again, so we can use the destructive recursive form.
404+
renderNodeDestructive(request, task, result);
405+
}
406+
392407
function renderElement(
393408
request: Request,
394409
task: Task,
@@ -397,38 +412,7 @@ function renderElement(
397412
node: ReactNodeList,
398413
): void {
399414
if (typeof type === 'function') {
400-
try {
401-
const result = type(props);
402-
renderNode(request, task, result);
403-
} catch (x) {
404-
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
405-
// Something suspended, we'll need to create a new segment and resolve it later.
406-
const segment = task.blockedSegment;
407-
const insertionIndex = segment.chunks.length;
408-
const newSegment = createPendingSegment(
409-
request,
410-
insertionIndex,
411-
null,
412-
segment.formatContext,
413-
);
414-
segment.children.push(newSegment);
415-
const newTask = createTask(
416-
request,
417-
node,
418-
task.blockedBoundary,
419-
newSegment,
420-
task.abortSet,
421-
task.assignID,
422-
);
423-
// We've delegated the assignment.
424-
task.assignID = null;
425-
const ping = newTask.ping;
426-
x.then(ping, ping);
427-
} else {
428-
// We can rethrow to terminate the rest of this tree.
429-
throw x;
430-
}
431-
}
415+
renderFunctionComponent(request, task, type, props);
432416
} else if (typeof type === 'string') {
433417
renderHostElement(request, task, type, props);
434418
} else if (type === REACT_SUSPENSE_TYPE) {
@@ -438,7 +422,17 @@ function renderElement(
438422
}
439423
}
440424

441-
function renderNode(request: Request, task: Task, node: ReactNodeList): void {
425+
// This function by it self renders a node and consumes the task by mutating it
426+
// to update the current execution state.
427+
function renderNodeDestructive(
428+
request: Request,
429+
task: Task,
430+
node: ReactNodeList,
431+
): void {
432+
// Stash the node we're working on. We'll pick up from this task in case
433+
// something suspends.
434+
task.node = node;
435+
442436
if (typeof node === 'string') {
443437
pushTextInstance(
444438
task.blockedSegment.chunks,
@@ -453,6 +447,9 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
453447
if (Array.isArray(node)) {
454448
if (node.length > 0) {
455449
for (let i = 0; i < node.length; i++) {
450+
// Recursively render the rest. We need to use the non-destructive form
451+
// so that we can safely pop back up and render the sibling if something
452+
// suspends.
456453
renderNode(request, task, node[i]);
457454
}
458455
} else {
@@ -486,6 +483,60 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
486483
throw new Error('Not yet implemented node type.');
487484
}
488485

486+
function spawnNewSuspendedTask(
487+
request: Request,
488+
task: Task,
489+
x: Promise<any>,
490+
): void {
491+
// Something suspended, we'll need to create a new segment and resolve it later.
492+
const segment = task.blockedSegment;
493+
const insertionIndex = segment.chunks.length;
494+
const newSegment = createPendingSegment(
495+
request,
496+
insertionIndex,
497+
null,
498+
segment.formatContext,
499+
);
500+
segment.children.push(newSegment);
501+
const newTask = createTask(
502+
request,
503+
task.node,
504+
task.blockedBoundary,
505+
newSegment,
506+
task.abortSet,
507+
task.assignID,
508+
);
509+
// We've delegated the assignment.
510+
task.assignID = null;
511+
const ping = newTask.ping;
512+
x.then(ping, ping);
513+
}
514+
515+
// This is a non-destructive form of rendering a node. If it suspends it spawns
516+
// a new task and restores the context of this task to what it was before.
517+
function renderNode(request: Request, task: Task, node: ReactNodeList): void {
518+
// TODO: Store segment.children.length here and reset it in case something
519+
// suspended partially through writing something.
520+
521+
// Snapshot the current context in case something throws to interrupt the
522+
// process.
523+
const previousContext = task.blockedSegment.formatContext;
524+
try {
525+
return renderNodeDestructive(request, task, node);
526+
} catch (x) {
527+
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
528+
spawnNewSuspendedTask(request, task, x);
529+
// Restore the context. We assume that this will be restored by the inner
530+
// functions in case nothing throws so we don't use "finally" here.
531+
task.blockedSegment.formatContext = previousContext;
532+
} else {
533+
// We assume that we don't need the correct context.
534+
// Let's terminate the rest of the tree and don't render any siblings.
535+
throw x;
536+
}
537+
}
538+
}
539+
489540
function erroredTask(
490541
request: Request,
491542
boundary: Root | SuspenseBoundary,
@@ -638,22 +689,9 @@ function retryTask(request: Request, task: Task): void {
638689
return;
639690
}
640691
try {
641-
let node = task.node;
642-
while (
643-
typeof node === 'object' &&
644-
node !== null &&
645-
(node: any).$$typeof === REACT_ELEMENT_TYPE &&
646-
typeof node.type === 'function'
647-
) {
648-
// Doing this here lets us reuse this same Segment if the next component
649-
// also suspends.
650-
const element: React$Element<any> = (node: any);
651-
task.node = node;
652-
// TODO: Classes and legacy context etc.
653-
node = element.type(element.props);
654-
}
655-
656-
renderNode(request, task, node);
692+
// We call the destructive form that mutates this task. That way if something
693+
// suspends again, we can reuse the same task instead of spawning a new one.
694+
renderNodeDestructive(request, task, task.node);
657695

658696
task.abortSet.delete(task);
659697
segment.status = COMPLETED;

0 commit comments

Comments
 (0)