@samitouri / QOS-React-1 / commits / 67fee58b1f

[Fizz] Start initial work immediately (#31079)

In a recent update we make Flight start working immediately rather than waitin for a new task. This commit updates fizz to have similar mechanics. We start the render in the currently running task but we do so in a microtask to avoid reentrancy. This aligns Fizz with Flight. ref: https://github.com/facebook/react/pull/30961

Josh Story committed Sep 26, 2024 at 13:51 UTC 67fee58b1f72754cc77488c40c44e786572ef954
3 files changed +160 -153
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+2 -2
@@ -8119,7 +8119,7 @@ describe('ReactDOMFizzServer', () => {
8119
8120 prerendering = false;
8121
8122 - const resumed = await ReactDOMFizzServer.resumeToPipeableStream(
8122 + const resumed = ReactDOMFizzServer.resumeToPipeableStream(
8123 <App />,
8124 JSON.parse(JSON.stringify(prerendered.postponed)),
8125 {
@@ -8187,7 +8187,7 @@ describe('ReactDOMFizzServer', () => {
8187 function onPostpone(reason) {
8188 postpones.push(reason);
8189 }
8190 - const result = await renderToPipeableStream(<App />, {
8190 + const result = renderToPipeableStream(<App />, {
8191 onError,
8192 onShellError,
8193 onPostpone,
packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
+124 -113
@@ -14,6 +14,7 @@ let Stream;
14 let React;
15 let ReactDOMFizzServer;
16 let Suspense;
17 +let act;
18
19 describe('ReactDOMFizzServerNode', () => {
20 beforeEach(() => {
@@ -22,6 +23,7 @@ describe('ReactDOMFizzServerNode', () => {
23 ReactDOMFizzServer = require('react-dom/server');
24 Stream = require('stream');
25 Suspense = React.Suspense;
26 + act = require('internal-test-utils').act;
27 });
28
29 function getTestWritable() {
@@ -54,54 +56,59 @@ describe('ReactDOMFizzServerNode', () => {
56 throw theInfinitePromise;
57 }
58
57 - it('should call renderToPipeableStream', () => {
59 + it('should call renderToPipeableStream', async () => {
60 const {writable, output} = getTestWritable();
59 - const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
60 - <div>hello world</div>,
61 - );
62 - pipe(writable);
63 - jest.runAllTimers();
61 + await act(() => {
62 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
63 + <div>hello world</div>,
64 + );
65 + pipe(writable);
66 + });
67 expect(output.result).toMatchInlineSnapshot(`"<div>hello world</div>"`);
68 });
69
67 - it('should emit DOCTYPE at the root of the document', () => {
70 + it('should emit DOCTYPE at the root of the document', async () => {
71 const {writable, output} = getTestWritable();
69 - const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
70 - <html>
71 - <body>hello world</body>
72 - </html>,
73 - );
74 - pipe(writable);
75 - jest.runAllTimers();
72 + await act(() => {
73 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
74 + <html>
75 + <body>hello world</body>
76 + </html>,
77 + );
78 + pipe(writable);
79 + });
80 // with Float, we emit empty heads if they are elided when rendering <html>
81 expect(output.result).toMatchInlineSnapshot(
82 `"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
83 );
84 });
85
82 - it('should emit bootstrap script src at the end', () => {
86 + it('should emit bootstrap script src at the end', async () => {
87 const {writable, output} = getTestWritable();
84 - const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
85 - <div>hello world</div>,
86 - {
87 - bootstrapScriptContent: 'INIT();',
88 - bootstrapScripts: ['init.js'],
89 - bootstrapModules: ['init.mjs'],
90 - },
91 - );
92 - pipe(writable);
93 - jest.runAllTimers();
88 + await act(() => {
89 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
90 + <div>hello world</div>,
91 + {
92 + bootstrapScriptContent: 'INIT();',
93 + bootstrapScripts: ['init.js'],
94 + bootstrapModules: ['init.mjs'],
95 + },
96 + );
97 + pipe(writable);
98 + });
99 expect(output.result).toMatchInlineSnapshot(
100 `"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
101 );
102 });
103
99 - it('should start writing after pipe', () => {
104 + it('should start writing after pipe', async () => {
105 const {writable, output} = getTestWritable();
101 - const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
102 - <div>hello world</div>,
103 - );
104 - jest.runAllTimers();
106 + let pipe;
107 + await act(() => {
108 + pipe = ReactDOMFizzServer.renderToPipeableStream(
109 + <div>hello world</div>,
110 + ).pipe;
111 + });
112 // First we write our header.
113 output.result +=
114 '<!doctype html><html><head><title>test</title><head><body>';
@@ -281,24 +288,26 @@ describe('ReactDOMFizzServerNode', () => {
288 let isCompleteCalls = 0;
289 const errors = [];
290 const {writable, output, completed} = getTestWritable();
284 - const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
285 - <div>
286 - <Suspense fallback={<div>Loading</div>}>
287 - <InfiniteSuspend />
288 - </Suspense>
289 - </div>,
290 - {
291 - onError(x) {
292 - errors.push(x.message);
293 - },
294 - onAllReady() {
295 - isCompleteCalls++;
291 + let abort;
292 + await act(() => {
293 + const pipeable = ReactDOMFizzServer.renderToPipeableStream(
294 + <div>
295 + <Suspense fallback={<div>Loading</div>}>
296 + <InfiniteSuspend />
297 + </Suspense>
298 + </div>,
299 + {
300 + onError(x) {
301 + errors.push(x.message);
302 + },
303 + onAllReady() {
304 + isCompleteCalls++;
305 + },
306 },
297 - },
298 - );
299 - pipe(writable);
300 -
301 - jest.runAllTimers();
307 + );
308 + pipeable.pipe(writable);
309 + abort = pipeable.abort;
310 + });
311
312 expect(output.result).toContain('Loading');
313 expect(isCompleteCalls).toBe(0);
@@ -360,26 +369,28 @@ describe('ReactDOMFizzServerNode', () => {
369 let isCompleteCalls = 0;
370 const errors = [];
371 const {writable, output, completed} = getTestWritable();
363 - const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
364 - <div>
365 - <Suspense fallback="Loading">
366 - <Suspense fallback={<InfiniteSuspend />}>
367 - <InfiniteSuspend />
372 + let abort;
373 + await act(() => {
374 + const pipeable = ReactDOMFizzServer.renderToPipeableStream(
375 + <div>
376 + <Suspense fallback="Loading">
377 + <Suspense fallback={<InfiniteSuspend />}>
378 + <InfiniteSuspend />
379 + </Suspense>
380 </Suspense>
369 - </Suspense>
370 - </div>,
371 - {
372 - onError(x) {
373 - errors.push(x.message);
374 - },
375 - onAllReady() {
376 - isCompleteCalls++;
381 + </div>,
382 + {
383 + onError(x) {
384 + errors.push(x.message);
385 + },
386 + onAllReady() {
387 + isCompleteCalls++;
388 + },
389 },
378 - },
379 - );
380 - pipe(writable);
381 -
382 - jest.runAllTimers();
390 + );
391 + pipeable.pipe(writable);
392 + abort = pipeable.abort;
393 + });
394
395 expect(output.result).toContain('Loading');
396 expect(isCompleteCalls).toBe(0);
@@ -428,15 +439,15 @@ describe('ReactDOMFizzServerNode', () => {
439
440 const client = new DelayClient();
441 const {writable, output, completed} = getTestWritable();
431 - ReactDOMFizzServer.renderToPipeableStream(
432 - <DelayContext.Provider value={client}>
433 - <Suspense fallback="loading">
434 - <Component />
435 - </Suspense>
436 - </DelayContext.Provider>,
437 - ).pipe(writable);
438 -
439 - jest.runAllTimers();
442 + await act(() => {
443 + ReactDOMFizzServer.renderToPipeableStream(
444 + <DelayContext.Provider value={client}>
445 + <Suspense fallback="loading">
446 + <Component />
447 + </Suspense>
448 + </DelayContext.Provider>,
449 + ).pipe(writable);
450 + });
451
452 expect(output.error).toBe(undefined);
453 expect(output.result).toContain('loading');
@@ -481,29 +492,28 @@ describe('ReactDOMFizzServerNode', () => {
492 output: output0,
493 completed: completed0,
494 } = getTestWritable();
484 - ReactDOMFizzServer.renderToPipeableStream(
485 - <DelayContext.Provider value={client0}>
486 - <Suspense fallback="loading">
487 - <Component />
488 - </Suspense>
489 - </DelayContext.Provider>,
490 - ).pipe(writable0);
491 -
495 const client1 = new DelayClient();
496 const {
497 writable: writable1,
498 output: output1,
499 completed: completed1,
500 } = getTestWritable();
498 - ReactDOMFizzServer.renderToPipeableStream(
499 - <DelayContext.Provider value={client1}>
500 - <Suspense fallback="loading">
501 - <Component />
502 - </Suspense>
503 - </DelayContext.Provider>,
504 - ).pipe(writable1);
505 -
506 - jest.runAllTimers();
501 + await act(() => {
502 + ReactDOMFizzServer.renderToPipeableStream(
503 + <DelayContext.Provider value={client0}>
504 + <Suspense fallback="loading">
505 + <Component />
506 + </Suspense>
507 + </DelayContext.Provider>,
508 + ).pipe(writable0);
509 + ReactDOMFizzServer.renderToPipeableStream(
510 + <DelayContext.Provider value={client1}>
511 + <Suspense fallback="loading">
512 + <Component />
513 + </Suspense>
514 + </DelayContext.Provider>,
515 + ).pipe(writable1);
516 + });
517
518 expect(output0.error).toBe(undefined);
519 expect(output0.result).toContain('loading');
@@ -552,22 +562,22 @@ describe('ReactDOMFizzServerNode', () => {
562
563 const client = new DelayClient();
564 const {writable, output, completed} = getTestWritable();
555 - ReactDOMFizzServer.renderToPipeableStream(
556 - <>
557 - <DelayContext.Provider value={client}>
558 - <Suspense fallback="loading">
559 - <Component />
560 - </Suspense>
561 - </DelayContext.Provider>
562 - <DelayContext.Provider value={client}>
563 - <Suspense fallback="loading">
564 - <Component />
565 - </Suspense>
566 - </DelayContext.Provider>
567 - </>,
568 - ).pipe(writable);
569 -
570 - jest.runAllTimers();
565 + await act(() => {
566 + ReactDOMFizzServer.renderToPipeableStream(
567 + <>
568 + <DelayContext.Provider value={client}>
569 + <Suspense fallback="loading">
570 + <Component />
571 + </Suspense>
572 + </DelayContext.Provider>
573 + <DelayContext.Provider value={client}>
574 + <Suspense fallback="loading">
575 + <Component />
576 + </Suspense>
577 + </DelayContext.Provider>
578 + </>,
579 + ).pipe(writable);
580 + });
581
582 expect(output.error).toBe(undefined);
583 expect(output.result).toContain('loading');
@@ -630,13 +640,14 @@ describe('ReactDOMFizzServerNode', () => {
640 expect(isComplete).toBe(true);
641 });
642
633 - it('should encode multibyte characters correctly without nulls (#24985)', () => {
643 + it('should encode multibyte characters correctly without nulls (#24985)', async () => {
644 const {writable, output} = getTestWritable();
635 - const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
636 - <div>{Array(700).fill('ののの')}</div>,
637 - );
638 - pipe(writable);
639 - jest.runAllTimers();
645 + await act(() => {
646 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
647 + <div>{Array(700).fill('ののの')}</div>,
648 + );
649 + pipe(writable);
650 + });
651 expect(output.result.indexOf('\u0000')).toBe(-1);
652 expect(output.result).toEqual(
653 '<div>' + Array(700).fill('ののの').join('<!-- -->') + '</div>',
packages/react-server/src/ReactFizzServer.js
+34 -38
@@ -316,10 +316,11 @@ type Segment = {
316 textEmbedded: boolean,
317 };
318
319 -const OPEN = 0;
320 -const ABORTING = 1;
321 -const CLOSING = 2;
322 -const CLOSED = 3;
319 +const OPENING = 10;
320 +const OPEN = 11;
321 +const ABORTING = 12;
322 +const CLOSING = 13;
323 +const CLOSED = 14;
324
325 export opaque type Request = {
326 destination: null | Destination,
@@ -328,7 +329,7 @@ export opaque type Request = {
329 +renderState: RenderState,
330 +rootFormatContext: FormatContext,
331 +progressiveChunkSize: number,
331 - status: 0 | 1 | 2 | 3,
332 + status: 10 | 11 | 12 | 13 | 14,
333 fatalError: mixed,
334 nextSegmentId: number,
335 allPendingTasks: number, // when it reaches zero, we can close the connection.
@@ -424,7 +425,7 @@ function RequestInstance(
425 progressiveChunkSize === undefined
426 ? DEFAULT_PROGRESSIVE_CHUNK_SIZE
427 : progressiveChunkSize;
427 - this.status = OPEN;
428 + this.status = OPENING;
429 this.fatalError = null;
430 this.nextSegmentId = 0;
431 this.allPendingTasks = 0;
@@ -688,7 +689,7 @@ function pingTask(request: Request, task: Task): void {
689 pingedTasks.push(task);
690 if (request.pingedTasks.length === 1) {
691 request.flushScheduled = request.destination !== null;
691 - if (request.trackedPostpones !== null) {
692 + if (request.trackedPostpones !== null || request.status === OPENING) {
693 scheduleMicrotask(() => performWork(request));
694 } else {
695 scheduleWork(() => performWork(request));
@@ -4977,43 +4978,38 @@ function flushCompletedQueues(
4978
4979 export function startWork(request: Request): void {
4980 request.flushScheduled = request.destination !== null;
4980 - if (request.trackedPostpones !== null) {
4981 - // When prerendering we use microtasks for pinging work
4982 - if (supportsRequestStorage) {
4983 - scheduleMicrotask(() =>
4984 - requestStorage.run(request, performWork, request),
4985 - );
4986 - } else {
4987 - scheduleMicrotask(() => performWork(request));
4988 - }
4981 + // When prerendering we use microtasks for pinging work
4982 + if (supportsRequestStorage) {
4983 + scheduleMicrotask(() => requestStorage.run(request, performWork, request));
4984 } else {
4990 - // When rendering/resuming we use regular tasks and we also emit early preloads
4991 - if (supportsRequestStorage) {
4992 - scheduleWork(() => requestStorage.run(request, performWork, request));
4993 - } else {
4994 - scheduleWork(() => performWork(request));
4985 + scheduleMicrotask(() => performWork(request));
4986 + }
4987 + scheduleWork(() => {
4988 + if (request.status === OPENING) {
4989 + request.status = OPEN;
4990 }
4996 - // this is either a regular render or a resume. For regular render we want
4997 - // to call emitEarlyPreloads after the first performWork because we want
4998 - // are responding to a live request and need to balance sending something early
4999 - // (i.e. don't want for the shell to finish) but we need something to send.
5000 - // The only implementation of this is for DOM at the moment and during resumes nothing
5001 - // actually emits but the code paths here are the same.
5002 - // During a prerender we don't want to be too aggressive in emitting early preloads
5003 - // because we aren't responding to a live request and we can wait for the prerender to
5004 - // postpone before we emit anything.
5005 - if (supportsRequestStorage) {
5006 - scheduleWork(() =>
4991 +
4992 + if (request.trackedPostpones === null) {
4993 + // this is either a regular render or a resume. For regular render we want
4994 + // to call emitEarlyPreloads after the first performWork because we want
4995 + // are responding to a live request and need to balance sending something early
4996 + // (i.e. don't want for the shell to finish) but we need something to send.
4997 + // The only implementation of this is for DOM at the moment and during resumes nothing
4998 + // actually emits but the code paths here are the same.
4999 + // During a prerender we don't want to be too aggressive in emitting early preloads
5000 + // because we aren't responding to a live request and we can wait for the prerender to
5001 + // postpone before we emit anything.
5002 + if (supportsRequestStorage) {
5003 requestStorage.run(
5004 request,
5005 enqueueEarlyPreloadsAfterInitialWork,
5006 request,
5011 - ),
5012 - );
5013 - } else {
5014 - scheduleWork(() => enqueueEarlyPreloadsAfterInitialWork(request));
5007 + );
5008 + } else {
5009 + enqueueEarlyPreloadsAfterInitialWork(request);
5010 + }
5011 }
5016 - }
5012 + });
5013 }
5014
5015 function enqueueEarlyPreloadsAfterInitialWork(request: Request) {
@@ -5095,7 +5091,7 @@ export function stopFlowing(request: Request): void {
5091
5092 // This is called to early terminate a request. It puts all pending boundaries in client rendered state.
5093 export function abort(request: Request, reason: mixed): void {
5098 - if (request.status === OPEN) {
5094 + if (request.status === OPEN || request.status === OPENING) {
5095 request.status = ABORTING;
5096 }
5097