@samitouri / QOS-React / commits / befc1246b0

[Fizz] Render preamble eagerly (#33730)

We unnecessarily render the preamble in a task. This updates the implementation to perform this render inline. Testing this is tricky because one of the only ways you could assert this was even happening is based on how things error if you abort while rendering the root. While adding a test for this I discovered that not all abortable tasks report errors when aborted during a normal render. I've asserted the current behavior and will address the other issue at another time and updated the assertion later as necessary

Josh Story committed Jul 8, 2025 at 11:20 UTC befc1246b07a04b401bc6e914b7f336a442dca1a
2 files changed +114 -24
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+96
@@ -9544,6 +9544,102 @@ describe('ReactDOMFizzServer', () => {
9544 );
9545 });
9546
9547 + it('will attempt to render the preamble inline to allow rendering before a later abort in the same task', async () => {
9548 + const promise = new Promise(() => {});
9549 + function Pending() {
9550 + React.use(promise);
9551 + }
9552 +
9553 + const controller = new AbortController();
9554 + function Abort() {
9555 + controller.abort();
9556 + return <Comp />;
9557 + }
9558 +
9559 + function Comp() {
9560 + return null;
9561 + }
9562 +
9563 + function App() {
9564 + return (
9565 + <html>
9566 + <head>
9567 + <meta content="here" />
9568 + </head>
9569 + <body>
9570 + <main>hello</main>
9571 + <Suspense>
9572 + <Pending />
9573 + </Suspense>
9574 + <Abort />
9575 + </body>
9576 + </html>
9577 + );
9578 + }
9579 +
9580 + const signal = controller.signal;
9581 +
9582 + let thrownError = null;
9583 + const errors = [];
9584 + try {
9585 + await act(() => {
9586 + const {pipe, abort} = renderToPipeableStream(<App />, {
9587 + onError(e, ei) {
9588 + errors.push({
9589 + error: e,
9590 + componentStack: normalizeCodeLocInfo(ei.componentStack),
9591 + });
9592 + },
9593 + });
9594 + signal.addEventListener('abort', () => abort('boom'));
9595 + pipe(writable);
9596 + });
9597 + } catch (e) {
9598 + thrownError = e;
9599 + }
9600 +
9601 + expect(thrownError).toBe('boom');
9602 + // TODO there should actually be three errors. One for the pending Suspense, one for the fallback task, and one for the task
9603 + // that does the abort itself. At the moment abort will flush queues and if there is no pending tasks will close the request before
9604 + // the task which initiated the abort can even be processed. This is a bug but not one that I am fixing with the current change
9605 + // so I am asserting the current behavior
9606 + expect(errors).toEqual([
9607 + {
9608 + error: 'boom',
9609 + componentStack: componentStack([
9610 + 'Pending',
9611 + 'Suspense',
9612 + 'body',
9613 + 'html',
9614 + 'App',
9615 + ]),
9616 + },
9617 + {
9618 + error: 'boom',
9619 + componentStack: componentStack([
9620 + 'Suspense Fallback',
9621 + 'body',
9622 + 'html',
9623 + 'App',
9624 + ]),
9625 + // }, {
9626 + // error: 'boom',
9627 + // componentStack: componentStack(['Abort', 'body', 'html', 'App'])
9628 + },
9629 + ]);
9630 +
9631 + // We expect the render to throw before streaming anything so the default
9632 + // document is still loaded
9633 + expect(getVisibleChildren(document)).toEqual(
9634 + <html>
9635 + <head />
9636 + <body>
9637 + <div id="container" />
9638 + </body>
9639 + </html>,
9640 + );
9641 + });
9642 +
9643 it('Will wait to flush Document chunks until all boundaries which might contain a preamble are errored or resolved', async () => {
9644 let rejectFirst;
9645 const firstPromise = new Promise((_, reject) => {
packages/react-server/src/ReactFizzServer.js
+18 -24
@@ -2206,7 +2206,7 @@ function renderSuspenseList(
2206
2207 function renderPreamble(
2208 request: Request,
2209 - task: Task,
2209 + task: RenderTask,
2210 blockedSegment: Segment,
2211 node: ReactNodeList,
2212 ): void {
@@ -2219,28 +2219,21 @@ function renderPreamble(
2219 false,
2220 );
2221 blockedSegment.preambleChildren.push(preambleSegment);
2222 - // @TODO we can just attempt to render in the current task rather than spawning a new one
2223 - const preambleTask = createRenderTask(
2224 - request,
2225 - null,
2226 - node,
2227 - -1,
2228 - task.blockedBoundary,
2229 - preambleSegment,
2230 - task.blockedPreamble,
2231 - task.hoistableState,
2232 - request.abortableTasks,
2233 - task.keyPath,
2234 - task.formatContext,
2235 - task.context,
2236 - task.treeContext,
2237 - task.row,
2238 - task.componentStack,
2239 - !disableLegacyContext ? task.legacyContext : emptyContextObject,
2240 - __DEV__ ? task.debugTask : null,
2241 - );
2242 - pushComponentStack(preambleTask);
2243 - request.pingedTasks.push(preambleTask);
2222 + task.blockedSegment = preambleSegment;
2223 + try {
2224 + preambleSegment.status = RENDERING;
2225 + renderNode(request, task, node, -1);
2226 + pushSegmentFinale(
2227 + preambleSegment.chunks,
2228 + request.renderState,
2229 + preambleSegment.lastPushedText,
2230 + preambleSegment.textEmbedded,
2231 + );
2232 + preambleSegment.status = COMPLETED;
2233 + finishedSegment(request, task.blockedBoundary, preambleSegment);
2234 + } finally {
2235 + task.blockedSegment = blockedSegment;
2236 + }
2237 }
2238
2239 function renderHostElement(
@@ -2292,7 +2285,8 @@ function renderHostElement(
2285 props,
2286 ));
2287 if (isPreambleContext(newContext)) {
2295 - renderPreamble(request, task, segment, children);
2288 + // $FlowFixMe: Refined
2289 + renderPreamble(request, (task: RenderTask), segment, children);
2290 } else {
2291 // We use the non-destructive form because if something suspends, we still
2292 // need to pop back up and finish this subtree of HTML.