Skip to content

Commit e7d31af

Browse files
committed
If we have exceeded our allocated space for inlining objects defer any objects
eligible to be a lazy reference (other lazy references and elements). This allows these to stream in on the client. We ping these instead of retrying them, which places them after the currently running task in the stream.
1 parent 5edd706 commit e7d31af

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,61 @@ describe('ReactFlightDOMEdge', () => {
160160
});
161161
}
162162

163+
function dripStream(input) {
164+
const reader = input.getReader();
165+
let nextDrop = 0;
166+
let controller = null;
167+
let streamDone = false;
168+
const buffer = [];
169+
function flush() {
170+
if (controller === null || nextDrop === 0) {
171+
return;
172+
}
173+
while (buffer.length > 0 && nextDrop > 0) {
174+
const nextChunk = buffer[0];
175+
if (nextChunk.byteLength <= nextDrop) {
176+
nextDrop -= nextChunk.byteLength;
177+
controller.enqueue(nextChunk);
178+
buffer.shift();
179+
if (streamDone && buffer.length === 0) {
180+
controller.done();
181+
}
182+
} else {
183+
controller.enqueue(nextChunk.subarray(0, nextDrop));
184+
buffer[0] = nextChunk.subarray(nextDrop);
185+
nextDrop = 0;
186+
}
187+
}
188+
}
189+
const output = new ReadableStream({
190+
start(c) {
191+
controller = c;
192+
async function pump() {
193+
for (;;) {
194+
const {value, done} = await reader.read();
195+
if (done) {
196+
streamDone = true;
197+
break;
198+
}
199+
buffer.push(value);
200+
flush();
201+
}
202+
}
203+
pump();
204+
},
205+
pull() {},
206+
cancel(reason) {
207+
reader.cancel(reason);
208+
},
209+
});
210+
function drip(n) {
211+
nextDrop += n;
212+
flush();
213+
}
214+
215+
return [output, drip];
216+
}
217+
163218
async function readResult(stream) {
164219
const reader = stream.getReader();
165220
let result = '';
@@ -576,6 +631,67 @@ describe('ReactFlightDOMEdge', () => {
576631
expect(serializedContent.length).toBeLessThan(150 + expectedDebugInfoSize);
577632
});
578633

634+
it('should break up large sync components by outlining into streamable elements', async () => {
635+
const paragraphs = [];
636+
for (let i = 0; i < 20; i++) {
637+
const text =
638+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris' +
639+
'porttitor tortor ac lectus faucibus, eget eleifend elit hendrerit.' +
640+
'Integer porttitor nisi in leo congue rutrum. Morbi sed ante posuere,' +
641+
'aliquam lorem ac, imperdiet orci. Duis malesuada gravida pharetra. Cras' +
642+
'facilisis arcu diam, id dictum lorem imperdiet a. Suspendisse aliquet' +
643+
'tempus tortor et ultricies. Aliquam libero velit, posuere tempus ante' +
644+
'sed, pellentesque tincidunt lorem. Nullam iaculis, eros a varius' +
645+
'aliquet, tortor felis tempor metus, nec cursus felis eros aliquam nulla.' +
646+
'Vivamus ut orci sed mauris congue lacinia. Cras eget blandit neque.' +
647+
'Pellentesque a massa in turpis ullamcorper volutpat vel at massa. Sed' +
648+
'ante est, auctor non diam non, vulputate ultrices metus. Maecenas dictum' +
649+
'fermentum quam id aliquam. Donec porta risus vitae pretium posuere.' +
650+
'Fusce facilisis eros in lacus tincidunt congue.' +
651+
i; /* trick dedupe */
652+
paragraphs.push(<p key={i}>{text}</p>);
653+
}
654+
655+
const stream = await serverAct(() =>
656+
ReactServerDOMServer.renderToReadableStream(paragraphs),
657+
);
658+
659+
const [stream2, drip] = dripStream(stream);
660+
661+
// Allow some of the content through.
662+
drip(5000);
663+
664+
const result = await ReactServerDOMClient.createFromReadableStream(
665+
stream2,
666+
{
667+
serverConsumerManifest: {
668+
moduleMap: null,
669+
moduleLoading: null,
670+
},
671+
},
672+
);
673+
674+
// We should have resolved enough to be able to get the array even though some
675+
// of the items inside are still lazy.
676+
expect(result.length).toBe(20);
677+
678+
// Unblock the rest
679+
drip(Infinity);
680+
681+
// Use the SSR render to resolve any lazy elements
682+
const ssrStream = await serverAct(() =>
683+
ReactDOMServer.renderToReadableStream(result),
684+
);
685+
const html = await readResult(ssrStream);
686+
687+
const ssrStream2 = await serverAct(() =>
688+
ReactDOMServer.renderToReadableStream(paragraphs),
689+
);
690+
const html2 = await readResult(ssrStream2);
691+
692+
expect(html).toBe(html2);
693+
});
694+
579695
it('should be able to serialize any kind of typed array', async () => {
580696
const buffer = new Uint8Array([
581697
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,

packages/react-server/src/ReactFlightServer.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,6 +1605,24 @@ let debugID: null | number = null;
16051605
let serializedSize = 0;
16061606
const MAX_ROW_SIZE = 3200;
16071607

1608+
function deferTask(request: Request, task: Task): ReactJSONValue {
1609+
// Like outlineTask but instead the item is scheduled to be serialized
1610+
// after its parent in the stream.
1611+
const newTask = createTask(
1612+
request,
1613+
task.model, // the currently rendering element
1614+
task.keyPath, // unlike outlineModel this one carries along context
1615+
task.implicitSlot,
1616+
request.abortableTasks,
1617+
__DEV__ ? task.debugOwner : null,
1618+
__DEV__ ? task.debugStack : null,
1619+
__DEV__ ? task.debugTask : null,
1620+
);
1621+
1622+
pingTask(request, newTask);
1623+
return serializeLazyID(newTask.id);
1624+
}
1625+
16081626
function outlineTask(request: Request, task: Task): ReactJSONValue {
16091627
const newTask = createTask(
16101628
request,
@@ -2449,6 +2467,10 @@ function renderModelDestructive(
24492467

24502468
const element: ReactElement = (value: any);
24512469

2470+
if (serializedSize > MAX_ROW_SIZE) {
2471+
return deferTask(request, task);
2472+
}
2473+
24522474
if (__DEV__) {
24532475
const debugInfo: ?ReactDebugInfo = (value: any)._debugInfo;
24542476
if (debugInfo) {
@@ -2507,6 +2529,10 @@ function renderModelDestructive(
25072529
return newChild;
25082530
}
25092531
case REACT_LAZY_TYPE: {
2532+
if (serializedSize > MAX_ROW_SIZE) {
2533+
return deferTask(request, task);
2534+
}
2535+
25102536
// Reset the task's thenable state before continuing. If there was one, it was
25112537
// from suspending the lazy before.
25122538
task.thenableState = null;

0 commit comments

Comments
 (0)