8000 [Flight] Defer Elements if the parent chunk is too large by sebmarkbage · Pull Request #33030 · facebook/react · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

[Flight] Defer Elements if the parent chunk is too large #33030

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign 8000 up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,61 @@ describe('ReactFlightDOMEdge', () => {
});
}

function dripStream(input) {
const reader = input.getReader();
let nextDrop = 0;
let controller = null;
let streamDone = false;
const buffer = [];
function flush() {
if (controller === null || nextDrop === 0) {
return;
}
while (buffer.length > 0 && nextDrop > 0) {
const nextChunk = buffer[0];
if (nextChunk.byteLength <= nextDrop) {
nextDrop -= nextChunk.byteLength;
controller.enqueue(nextChunk);
buffer.shift();
if (streamDone && buffer.length === 0) {
controller.done();
}
} else {
controller.enqueue(nextChunk.subarray(0, nextDrop));
buffer[0] = nextChunk.subarray(nextDrop);
nextDrop = 0;
}
}
}
const output = new ReadableStream({
start(c) {
controller = c;
async function pump() {
for (;;) {
const {value, done} = await reader.read();
if (done) {
streamDone = true;
break;
}
buffer.push(value);
flush();
}
}
pump();
},
pull() {},
cancel(reason) {
reader.cancel(reason);
},
});
function drip(n) {
nextDrop += n;
flush();
}

return [output, drip];
}

async function readResult(stream) {
const reader = stream.getReader();
let result = '';
Expand Down Expand Up @@ -576,6 +631,67 @@ describe('ReactFlightDOMEdge', () => {
expect(serializedContent.length).toBeLessThan(150 + expectedDebugInfoSize);
});

it('should break up large sync components by outlining into streamable elements', async () => {
const paragraphs = [];
for (let i = 0; i < 20; i++) {
const text =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris' +
'porttitor tortor ac lectus faucibus, eget eleifend elit hendrerit.' +
'Integer porttitor nisi in leo congue rutrum. Morbi sed ante posuere,' +
'aliquam lorem ac, imperdiet orci. Duis malesuada gravida pharetra. Cras' +
'facilisis arcu diam, id dictum lorem imperdiet a. Suspendisse aliquet' +
'tempus tortor et ultricies. Aliquam libero velit, posuere tempus ante' +
'sed, pellentesque tincidunt lorem. Nullam iaculis, eros a varius' +
'aliquet, tortor felis tempor metus, nec cursus felis eros aliquam nulla.' +
'Vivamus ut orci sed mauris congue lacinia. Cras eget blandit neque.' +
'Pellentesque a massa in turpis ullamcorper volutpat vel at massa. Sed' +
'ante est, auctor non diam non, vulputate ultrices metus. Maecenas dictum' +
'fermentum quam id aliquam. Donec porta risus vitae pretium posuere.' +
'Fusce facilisis eros in lacus tincidunt congue.' +
i; /* trick dedupe */
paragraphs.push(<p key={i}>{text}</p>);
}

const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(paragraphs),
);

const [stream2, drip] = dripStream(stream);

// Allow some of the content through.
drip(5000);

const result = await ReactServerDOMClient.createFromReadableStream(
stream2,
{
serverConsumerManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);

// We should have resolved enough to be able to get the array even though some
// of the items inside are still lazy.
expect(result.length).toBe(20);

// Unblock the rest
drip(Infinity);

// Use the SSR render to resolve any lazy elements
const ssrStream = await serverAct(() =>
ReactDOMServer.renderToReadableStream(result),
);
const html = await readResult(ssrStream);

const ssrStream2 = await serverAct(() =>
ReactDOMServer.renderToReadableStream(paragraphs),
);
const html2 = await readResult(ssrStream2);

expect(html).toBe(html2);
});

it('should be able to serialize any kind of typed array', async () => {
const buffer = new Uint8Array([
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
Expand Down
49 changes: 46 additions & 3 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1600,6 +1600,29 @@ function renderClientElement(
// The chunk ID we're currently rendering that we can assign debug data to.
let debugID: null | number = null;

// Approximate string length of the currently serializing row.
// Used to power outlining heuristics.
let serializedSize = 0;
const MAX_ROW_SIZE = 3200;

function deferTask(request: Request, task: Task): ReactJSONValue {
// Like outlineTask but instead the item is scheduled to be serialized
// after its parent in the stream.
const newTask = createTask(
request,
task.model, // the currently rendering element
task.keyPath, // unlike outlineModel this one carries along context
task.implicitSlot,
request.abortableTasks,
__DEV__ ? task.debugOwner : null,
__DEV__ ? task.debugStack : null,
__DEV__ ? task.debugTask : null,
);

pingTask(request, newTask);
return serializeLazyID(newTask.id);
}

function outlineTask(request: Request, task: Task): ReactJSONValue {
const newTask = createTask(
request,
Expand Down Expand Up @@ -2393,6 +2416,8 @@ function renderModelDestructive(
// Set the currently rendering model
task.model = value;

serializedSize += parentPropertyName.length;

// Special Symbol, that's very common.
if (value === REACT_ELEMENT_TYPE) {
return '$';
Expand Down Expand Up @@ -2442,6 +2467,10 @@ function renderModelDestructive(

const element: ReactElement = (value: any);

if (serializedSize > MAX_ROW_SIZE) {
return deferTask(request, task);
}

if (__DEV__) {
const debugInfo: ?ReactDebugInfo = (value: any)._debugInfo;
if (debugInfo) {
Expand Down Expand Up @@ -2500,6 +2529,10 @@ function renderModelDestructive(
return newChild;
}
case REACT_LAZY_TYPE: {
if (serializedSize > MAX_ROW_SIZE) {
return deferTask(request, task);
}

// Reset the task's thenable state before continuing. If there was one, it was
// from suspending the lazy before.
task.thenableState = null;
Expand Down Expand Up @@ -2811,6 +2844,7 @@ function renderModelDestructive(
throwTaintViolation(tainted.message);
}
}
serializedSize += value.length;
// TODO: Maybe too clever. If we support URL there's no similar trick.
if (value[value.length - 1] === 'Z') {
// Possibly a Date, whose toJSON automatically calls toISOString
Expand Down Expand Up @@ -3892,9 +3926,18 @@ function emitChunk(
return;
}
// For anything else we need to try to serialize it using JSON.
// $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
const json: string = stringify(value, task.toJSON);
emitModelChunk(request, task.id, json);
// We stash the outer parent size so we can restore it when we exit.
const parentSerializedSize = serializedSize;
// We don't reset the serialized size counter from reentry because that indicates that we
// are outlining a model and we actually want to include that size into the parent since
// it will still block the parent row. It only restores to zero at the top of the stack.
try {
// $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
const json: string = stringify(value, task.toJSON);
emitModelChunk(request, task.id, json);
} finally {
serializedSize = parentSerializedSize;
}
}

function erroredTask(request: Request, task: Task, error: mixed): void {
Expand Down
Loading
0