Commit 47d1ad1

Browse files
authored
[Flight] Skip transferReferencedDebugInfo during debug info resolution (#35795)
When the Flight Client resolves chunk references during model parsing, it calls `transferReferencedDebugInfo` to propagate debug info entries from referenced chunks to the parent chunk. Debug info on chunks is later moved to their resolved values, where it is used by React DevTools to show performance tracks and what a component was suspended by. Debug chunks themselves (specifically `ReactComponentInfo`, `ReactAsyncInfo`, `ReactIOInfo`, and their outlined references) are metadata that is never rendered. They don't need debug info attached to them. Without this fix, debug info entries accumulate on outlined debug chunks via their references to other debug chunks (e.g. owner chains and props deduplication paths). Since each outlined chunk's accumulated entries are copied to every chunk that references it, this creates exponential growth in deep component trees, which can cause the dev server to hang and run out of memory. This generalizes the existing skip of `transferReferencedDebugInfo` for Element owner/stack references (which already recognizes that references to debug chunks don't need debug info transferred) to all references resolved during debug info resolution. It adds an `isInitializingDebugInfo` flag set in `initializeDebugChunk` and `resolveIOInfo`, which propagates through all nested `initializeModelChunk` calls within the same synchronous stack. For the async path, `waitForReference` captures the flag at call time into `InitializationReference.isDebug`, so deferred fulfillments also skip the transfer.
1 parent e8c6362 commit 47d1ad1

2 files changed

Lines changed: 77 additions & 19 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,7 @@ type InitializationHandler = {
943943
};
944944
let initializingHandler: null | InitializationHandler = null;
945945
let initializingChunk: null | BlockedChunk<any> = null;
946+
let isInitializingDebugInfo: boolean = false;
946947

947948
function initializeDebugChunk(
948949
response: Response,
@@ -951,6 +952,8 @@ function initializeDebugChunk(
951952
constdebugChunk=chunk._debugChunk;
952953
if(debugChunk!==null){
953954
const debugInfo =chunk._debugInfo;
955+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
956+
isInitializingDebugInfo=true;
954957
try{
955958
if(debugChunk.status===RESOLVED_MODEL){
956959
// Find the index of this debug info by walking the linked list.
@@ -1015,6 +1018,8 @@ function initializeDebugChunk(
10151018
}
10161019
}catch(error){
10171020
triggerErrorOnChunk(response,chunk,error);
1021+
}finally{
1022+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
10181023
}
10191024
}
10201025
}
@@ -1632,7 +1637,9 @@ function fulfillReference(
16321637
constelement: any=handler.value;
16331638
switch(key){
16341639
case'3':
1635-
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1640+
if(__DEV__){
1641+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1642+
}
16361643
element.props=mappedValue;
16371644
break;
16381645
case'4':
@@ -1648,7 +1655,9 @@ function fulfillReference(
16481655
}
16491656
break;
16501657
default:
1651-
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
1658+
if (__DEV__) {
1659+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1660+
}
16521661
break;
16531662
}
16541663
}elseif(__DEV__&&!reference.isDebug){
@@ -2086,7 +2095,7 @@ function getOutlinedModel<T>(
20862095
response,
20872096
map,
20882097
path.slice(i-1),
2089-
false,
2098+
isInitializingDebugInfo,
20902099
);
20912100
}
20922101
case HALTED: {
@@ -2158,14 +2167,21 @@ function getOutlinedModel<T>(
21582167
}
21592168

21602169
const chunkValue = map(response, value, parentObject, key);
2161-
if (
2162-
parentObject[0] === REACT_ELEMENT_TYPE &&
2163-
(key=== '4' ||key=== '5')
2164-
){
2165-
// If we're resolving the "owner" or "stack" slot of an Element array, we don't call
2166-
// transferReferencedDebugInfo because this reference is to a debug chunk.
2167-
} else {
2168-
transferReferencedDebugInfo(initializingChunk,chunk);
2170+
if (__DEV__) {
2171+
if(
2172+
parentObject[0]===REACT_ELEMENT_TYPE&&
2173+
(key==='4'||key==='5')
2174+
){
2175+
// If we're resolving the "owner" or "stack" slot of an Element array,
2176+
// we don't call transferReferencedDebugInfo because this reference is
2177+
// to a debug chunk.
2178+
} else if (isInitializingDebugInfo) {
2179+
// If we're resolving references as part of debug info resolution, we
2180+
// don't call transferReferencedDebugInfo because these references are
2181+
// to debug chunks.
2182+
} else {
2183+
transferReferencedDebugInfo(initializingChunk,chunk);
2184+
}
21692185
}
21702186
returnchunkValue;
21712187
casePENDING:
@@ -2177,7 +2193,7 @@ function getOutlinedModel<T>(
21772193
response,
21782194
map,
21792195
path,
2180-
false,
2196+
isInitializingDebugInfo,
21812197
);
21822198
caseHALTED: {
21832199
// Add a dependency that will never resolve.
@@ -4264,15 +4280,21 @@ function resolveIOInfo(
42644280
): void{
42654281
constchunks=response._chunks;
42664282
letchunk=chunks.get(id);
4267-
if(!chunk){
4268-
chunk =createResolvedModelChunk(response,model);
4269-
chunks.set(id,chunk);
4270-
initializeModelChunk(chunk);
4271-
}else{
4272-
resolveModelChunk(response,chunk,model);
4273-
if(chunk.status===RESOLVED_MODEL){
4283+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
4284+
isInitializingDebugInfo=true;
4285+
try{
4286+
if(!chunk){
4287+
chunk=createResolvedModelChunk(response,model);
4288+
chunks.set(id,chunk);
42744289
initializeModelChunk(chunk);
4290+
}else{
4291+
resolveModelChunk(response,chunk,model);
4292+
if(chunk.status===RESOLVED_MODEL){
4293+
initializeModelChunk(chunk);
4294+
}
42754295
}
4296+
} finally {
4297+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
42764298
}
42774299
if (chunk.status === INITIALIZED) {
42784300
initializeIOInfo(response,chunk.value);

‎packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3633,4 +3633,40 @@ describe('ReactFlightAsyncDebugInfo', () => {
36333633
`);
36343634
}
36353635
});
3636+
3637+
it('should not exponentially accumulate debug info on outlined debug chunks',async()=>{
3638+
// Regression test: Each Level wraps its received `context` prop in a new
3639+
// object before passing it down. This creates props deduplication
3640+
// references to the parent's outlined chunk alongside the owner reference,
3641+
// giving 2 references per level to the direct parent's chunk. Without
3642+
// skipping transferReferencedDebugInfo during debug info resolution, this
3643+
// test would fail with an infinite loop detection error.
3644+
asyncfunctionLevel({depth, context}){
3645+
awaitdelay(0);
3646+
if(depth===0){
3647+
return<div>Hello, World!</div>;
3648+
}
3649+
constnewContext={prev: context,id: depth};
3650+
returnReactServer.createElement(Level,{
3651+
depth: depth-1,
3652+
context: newContext,
3653+
});
3654+
}
3655+
3656+
conststream=ReactServerDOMServer.renderToPipeableStream(
3657+
ReactServer.createElement(Level,{depth: 20,context: {root: true}}),
3658+
);
3659+
3660+
constreadable=newStream.PassThrough(streamOptions);
3661+
constresult=ReactServerDOMClient.createFromNodeStream(readable,{
3662+
moduleMap: {},
3663+
moduleLoading: {},
3664+
});
3665+
stream.pipe(readable);
3666+
3667+
constresolved=awaitresult;
3668+
expect(resolved.type).toBe('div');
3669+
3670+
awaitfinishLoadingStream(readable);
3671+
});
36363672
});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Commit 47d1ad1

Browse files
authored
[Flight] Skip transferReferencedDebugInfo during debug info resolution (#35795)
When the Flight Client resolves chunk references during model parsing, it calls `transferReferencedDebugInfo` to propagate debug info entries from referenced chunks to the parent chunk. Debug info on chunks is later moved to their resolved values, where it is used by React DevTools to show performance tracks and what a component was suspended by. Debug chunks themselves (specifically `ReactComponentInfo`, `ReactAsyncInfo`, `ReactIOInfo`, and their outlined references) are metadata that is never rendered. They don't need debug info attached to them. Without this fix, debug info entries accumulate on outlined debug chunks via their references to other debug chunks (e.g. owner chains and props deduplication paths). Since each outlined chunk's accumulated entries are copied to every chunk that references it, this creates exponential growth in deep component trees, which can cause the dev server to hang and run out of memory. This generalizes the existing skip of `transferReferencedDebugInfo` for Element owner/stack references (which already recognizes that references to debug chunks don't need debug info transferred) to all references resolved during debug info resolution. It adds an `isInitializingDebugInfo` flag set in `initializeDebugChunk` and `resolveIOInfo`, which propagates through all nested `initializeModelChunk` calls within the same synchronous stack. For the async path, `waitForReference` captures the flag at call time into `InitializationReference.isDebug`, so deferred fulfillments also skip the transfer.
1 parent e8c6362 commit 47d1ad1

2 files changed

Lines changed: 77 additions & 19 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,7 @@ type InitializationHandler = {
943943
};
944944
let initializingHandler: null | InitializationHandler = null;
945945
let initializingChunk: null | BlockedChunk<any> = null;
946+
let isInitializingDebugInfo: boolean = false;
946947

947948
function initializeDebugChunk(
948949
response: Response,
@@ -951,6 +952,8 @@ function initializeDebugChunk(
951952
constdebugChunk=chunk._debugChunk;
952953
if(debugChunk!==null){
953954
const debugInfo =chunk._debugInfo;
955+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
956+
isInitializingDebugInfo=true;
954957
try{
955958
if(debugChunk.status===RESOLVED_MODEL){
956959
// Find the index of this debug info by walking the linked list.
@@ -1015,6 +1018,8 @@ function initializeDebugChunk(
10151018
}
10161019
}catch(error){
10171020
triggerErrorOnChunk(response,chunk,error);
1021+
}finally{
1022+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
10181023
}
10191024
}
10201025
}
@@ -1632,7 +1637,9 @@ function fulfillReference(
16321637
constelement: any=handler.value;
16331638
switch(key){
16341639
case'3':
1635-
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1640+
if(__DEV__){
1641+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1642+
}
16361643
element.props=mappedValue;
16371644
break;
16381645
case'4':
@@ -1648,7 +1655,9 @@ function fulfillReference(
16481655
}
16491656
break;
16501657
default:
1651-
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
1658+
if (__DEV__) {
1659+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1660+
}
16521661
break;
16531662
}
16541663
}elseif(__DEV__&&!reference.isDebug){
@@ -2086,7 +2095,7 @@ function getOutlinedModel<T>(
20862095
response,
20872096
map,
20882097
path.slice(i-1),
2089-
false,
2098+
isInitializingDebugInfo,
20902099
);
20912100
}
20922101
case HALTED: {
@@ -2158,14 +2167,21 @@ function getOutlinedModel<T>(
21582167
}
21592168

21602169
const chunkValue = map(response, value, parentObject, key);
2161-
if (
2162-
parentObject[0] === REACT_ELEMENT_TYPE &&
2163-
(key=== '4' ||key=== '5')
2164-
){
2165-
// If we're resolving the "owner" or "stack" slot of an Element array, we don't call
2166-
// transferReferencedDebugInfo because this reference is to a debug chunk.
2167-
} else {
2168-
transferReferencedDebugInfo(initializingChunk,chunk);
2170+
if (__DEV__) {
2171+
if(
2172+
parentObject[0]===REACT_ELEMENT_TYPE&&
2173+
(key==='4'||key==='5')
2174+
){
2175+
// If we're resolving the "owner" or "stack" slot of an Element array,
2176+
// we don't call transferReferencedDebugInfo because this reference is
2177+
// to a debug chunk.
2178+
} else if (isInitializingDebugInfo) {
2179+
// If we're resolving references as part of debug info resolution, we
2180+
// don't call transferReferencedDebugInfo because these references are
2181+
// to debug chunks.
2182+
} else {
2183+
transferReferencedDebugInfo(initializingChunk,chunk);
2184+
}
21692185
}
21702186
returnchunkValue;
21712187
casePENDING:
@@ -2177,7 +2193,7 @@ function getOutlinedModel<T>(
21772193
response,
21782194
map,
21792195
path,
2180-
false,
2196+
isInitializingDebugInfo,
21812197
);
21822198
caseHALTED: {
21832199
// Add a dependency that will never resolve.
@@ -4264,15 +4280,21 @@ function resolveIOInfo(
42644280
): void{
42654281
constchunks=response._chunks;
42664282
letchunk=chunks.get(id);
4267-
if(!chunk){
4268-
chunk =createResolvedModelChunk(response,model);
4269-
chunks.set(id,chunk);
4270-
initializeModelChunk(chunk);
4271-
}else{
4272-
resolveModelChunk(response,chunk,model);
4273-
if(chunk.status===RESOLVED_MODEL){
4283+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
4284+
isInitializingDebugInfo=true;
4285+
try{
4286+
if(!chunk){
4287+
chunk=createResolvedModelChunk(response,model);
4288+
chunks.set(id,chunk);
42744289
initializeModelChunk(chunk);
4290+
}else{
4291+
resolveModelChunk(response,chunk,model);
4292+
if(chunk.status===RESOLVED_MODEL){
4293+
initializeModelChunk(chunk);
4294+
}
42754295
}
4296+
} finally {
4297+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
42764298
}
42774299
if (chunk.status === INITIALIZED) {
42784300
initializeIOInfo(response,chunk.value);

‎packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3633,4 +3633,40 @@ describe('ReactFlightAsyncDebugInfo', () => {
36333633
`);
36343634
}
36353635
});
3636+
3637+
it('should not exponentially accumulate debug info on outlined debug chunks',async()=>{
3638+
// Regression test: Each Level wraps its received `context` prop in a new
3639+
// object before passing it down. This creates props deduplication
3640+
// references to the parent's outlined chunk alongside the owner reference,
3641+
// giving 2 references per level to the direct parent's chunk. Without
3642+
// skipping transferReferencedDebugInfo during debug info resolution, this
3643+
// test would fail with an infinite loop detection error.
3644+
asyncfunctionLevel({depth, context}){
3645+
awaitdelay(0);
3646+
if(depth===0){
3647+
return<div>Hello, World!</div>;
3648+
}
3649+
constnewContext={prev: context,id: depth};
3650+
returnReactServer.createElement(Level,{
3651+
depth: depth-1,
3652+
context: newContext,
3653+
});
3654+
}
3655+
3656+
conststream=ReactServerDOMServer.renderToPipeableStream(
3657+
ReactServer.createElement(Level,{depth: 20,context: {root: true}}),
3658+
);
3659+
3660+
constreadable=newStream.PassThrough(streamOptions);
3661+
constresult=ReactServerDOMClient.createFromNodeStream(readable,{
3662+
moduleMap: {},
3663+
moduleLoading: {},
3664+
});
3665+
stream.pipe(readable);
3666+
3667+
constresolved=awaitresult;
3668+
expect(resolved.type).toBe('div');
3669+
3670+
awaitfinishLoadingStream(readable);
3671+
});
36363672
});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 47d1ad1

Browse files
authored
[Flight] Skip transferReferencedDebugInfo during debug info resolution (#35795)
When the Flight Client resolves chunk references during model parsing, it calls `transferReferencedDebugInfo` to propagate debug info entries from referenced chunks to the parent chunk. Debug info on chunks is later moved to their resolved values, where it is used by React DevTools to show performance tracks and what a component was suspended by. Debug chunks themselves (specifically `ReactComponentInfo`, `ReactAsyncInfo`, `ReactIOInfo`, and their outlined references) are metadata that is never rendered. They don't need debug info attached to them. Without this fix, debug info entries accumulate on outlined debug chunks via their references to other debug chunks (e.g. owner chains and props deduplication paths). Since each outlined chunk's accumulated entries are copied to every chunk that references it, this creates exponential growth in deep component trees, which can cause the dev server to hang and run out of memory. This generalizes the existing skip of `transferReferencedDebugInfo` for Element owner/stack references (which already recognizes that references to debug chunks don't need debug info transferred) to all references resolved during debug info resolution. It adds an `isInitializingDebugInfo` flag set in `initializeDebugChunk` and `resolveIOInfo`, which propagates through all nested `initializeModelChunk` calls within the same synchronous stack. For the async path, `waitForReference` captures the flag at call time into `InitializationReference.isDebug`, so deferred fulfillments also skip the transfer.
1 parent e8c6362 commit 47d1ad1

2 files changed

Lines changed: 77 additions & 19 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,7 @@ type InitializationHandler = {
943943
};
944944
let initializingHandler: null | InitializationHandler = null;
945945
let initializingChunk: null | BlockedChunk<any> = null;
946+
let isInitializingDebugInfo: boolean = false;
946947

947948
function initializeDebugChunk(
948949
response: Response,
@@ -951,6 +952,8 @@ function initializeDebugChunk(
951952
constdebugChunk=chunk._debugChunk;
952953
if(debugChunk!==null){
953954
const debugInfo =chunk._debugInfo;
955+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
956+
isInitializingDebugInfo=true;
954957
try{
955958
if(debugChunk.status===RESOLVED_MODEL){
956959
// Find the index of this debug info by walking the linked list.
@@ -1015,6 +1018,8 @@ function initializeDebugChunk(
10151018
}
10161019
}catch(error){
10171020
triggerErrorOnChunk(response,chunk,error);
1021+
}finally{
1022+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
10181023
}
10191024
}
10201025
}
@@ -1632,7 +1637,9 @@ function fulfillReference(
16321637
constelement: any=handler.value;
16331638
switch(key){
16341639
case'3':
1635-
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1640+
if(__DEV__){
1641+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1642+
}
16361643
element.props=mappedValue;
16371644
break;
16381645
case'4':
@@ -1648,7 +1655,9 @@ function fulfillReference(
16481655
}
16491656
break;
16501657
default:
1651-
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
1658+
if (__DEV__) {
1659+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1660+
}
16521661
break;
16531662
}
16541663
}elseif(__DEV__&&!reference.isDebug){
@@ -2086,7 +2095,7 @@ function getOutlinedModel<T>(
20862095
response,
20872096
map,
20882097
path.slice(i-1),
2089-
false,
2098+
isInitializingDebugInfo,
20902099
);
20912100
}
20922101
case HALTED: {
@@ -2158,14 +2167,21 @@ function getOutlinedModel<T>(
21582167
}
21592168

21602169
const chunkValue = map(response, value, parentObject, key);
2161-
if (
2162-
parentObject[0] === REACT_ELEMENT_TYPE &&
2163-
(key=== '4' ||key=== '5')
2164-
){
2165-
// If we're resolving the "owner" or "stack" slot of an Element array, we don't call
2166-
// transferReferencedDebugInfo because this reference is to a debug chunk.
2167-
} else {
2168-
transferReferencedDebugInfo(initializingChunk,chunk);
2170+
if (__DEV__) {
2171+
if(
2172+
parentObject[0]===REACT_ELEMENT_TYPE&&
2173+
(key==='4'||key==='5')
2174+
){
2175+
// If we're resolving the "owner" or "stack" slot of an Element array,
2176+
// we don't call transferReferencedDebugInfo because this reference is
2177+
// to a debug chunk.
2178+
} else if (isInitializingDebugInfo) {
2179+
// If we're resolving references as part of debug info resolution, we
2180+
// don't call transferReferencedDebugInfo because these references are
2181+
// to debug chunks.
2182+
} else {
2183+
transferReferencedDebugInfo(initializingChunk,chunk);
2184+
}
21692185
}
21702186
returnchunkValue;
21712187
casePENDING:
@@ -2177,7 +2193,7 @@ function getOutlinedModel<T>(
21772193
response,
21782194
map,
21792195
path,
2180-
false,
2196+
isInitializingDebugInfo,
21812197
);
21822198
caseHALTED: {
21832199
// Add a dependency that will never resolve.
@@ -4264,15 +4280,21 @@ function resolveIOInfo(
42644280
): void{
42654281
constchunks=response._chunks;
42664282
letchunk=chunks.get(id);
4267-
if(!chunk){
4268-
chunk =createResolvedModelChunk(response,model);
4269-
chunks.set(id,chunk);
4270-
initializeModelChunk(chunk);
4271-
}else{
4272-
resolveModelChunk(response,chunk,model);
4273-
if(chunk.status===RESOLVED_MODEL){
4283+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
4284+
isInitializingDebugInfo=true;
4285+
try{
4286+
if(!chunk){
4287+
chunk=createResolvedModelChunk(response,model);
4288+
chunks.set(id,chunk);
42744289
initializeModelChunk(chunk);
4290+
}else{
4291+
resolveModelChunk(response,chunk,model);
4292+
if(chunk.status===RESOLVED_MODEL){
4293+
initializeModelChunk(chunk);
4294+
}
42754295
}
4296+
} finally {
4297+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
42764298
}
42774299
if (chunk.status === INITIALIZED) {
42784300
initializeIOInfo(response,chunk.value);

‎packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3633,4 +3633,40 @@ describe('ReactFlightAsyncDebugInfo', () => {
36333633
`);
36343634
}
36353635
});
3636+
3637+
it('should not exponentially accumulate debug info on outlined debug chunks',async()=>{
3638+
// Regression test: Each Level wraps its received `context` prop in a new
3639+
// object before passing it down. This creates props deduplication
3640+
// references to the parent's outlined chunk alongside the owner reference,
3641+
// giving 2 references per level to the direct parent's chunk. Without
3642+
// skipping transferReferencedDebugInfo during debug info resolution, this
3643+
// test would fail with an infinite loop detection error.
3644+
asyncfunctionLevel({depth, context}){
3645+
awaitdelay(0);
3646+
if(depth===0){
3647+
return<div>Hello, World!</div>;
3648+
}
3649+
constnewContext={prev: context,id: depth};
3650+
returnReactServer.createElement(Level,{
3651+
depth: depth-1,
3652+
context: newContext,
3653+
});
3654+
}
3655+
3656+
conststream=ReactServerDOMServer.renderToPipeableStream(
3657+
ReactServer.createElement(Level,{depth: 20,context: {root: true}}),
3658+
);
3659+
3660+
constreadable=newStream.PassThrough(streamOptions);
3661+
constresult=ReactServerDOMClient.createFromNodeStream(readable,{
3662+
moduleMap: {},
3663+
moduleLoading: {},
3664+
});
3665+
stream.pipe(readable);
3666+
3667+
constresolved=awaitresult;
3668+
expect(resolved.type).toBe('div');
3669+
3670+
awaitfinishLoadingStream(readable);
3671+
});
36363672
});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 47d1ad1

Browse files
authored
[Flight] Skip transferReferencedDebugInfo during debug info resolution (#35795)
When the Flight Client resolves chunk references during model parsing, it calls `transferReferencedDebugInfo` to propagate debug info entries from referenced chunks to the parent chunk. Debug info on chunks is later moved to their resolved values, where it is used by React DevTools to show performance tracks and what a component was suspended by. Debug chunks themselves (specifically `ReactComponentInfo`, `ReactAsyncInfo`, `ReactIOInfo`, and their outlined references) are metadata that is never rendered. They don't need debug info attached to them. Without this fix, debug info entries accumulate on outlined debug chunks via their references to other debug chunks (e.g. owner chains and props deduplication paths). Since each outlined chunk's accumulated entries are copied to every chunk that references it, this creates exponential growth in deep component trees, which can cause the dev server to hang and run out of memory. This generalizes the existing skip of `transferReferencedDebugInfo` for Element owner/stack references (which already recognizes that references to debug chunks don't need debug info transferred) to all references resolved during debug info resolution. It adds an `isInitializingDebugInfo` flag set in `initializeDebugChunk` and `resolveIOInfo`, which propagates through all nested `initializeModelChunk` calls within the same synchronous stack. For the async path, `waitForReference` captures the flag at call time into `InitializationReference.isDebug`, so deferred fulfillments also skip the transfer.
1 parent e8c6362 commit 47d1ad1

2 files changed

Lines changed: 77 additions & 19 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,7 @@ type InitializationHandler = {
943943
};
944944
let initializingHandler: null | InitializationHandler = null;
945945
let initializingChunk: null | BlockedChunk<any> = null;
946+
let isInitializingDebugInfo: boolean = false;
946947

947948
function initializeDebugChunk(
948949
response: Response,
@@ -951,6 +952,8 @@ function initializeDebugChunk(
951952
constdebugChunk=chunk._debugChunk;
952953
if(debugChunk!==null){
953954
const debugInfo =chunk._debugInfo;
955+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
956+
isInitializingDebugInfo=true;
954957
try{
955958
if(debugChunk.status===RESOLVED_MODEL){
956959
// Find the index of this debug info by walking the linked list.
@@ -1015,6 +1018,8 @@ function initializeDebugChunk(
10151018
}
10161019
}catch(error){
10171020
triggerErrorOnChunk(response,chunk,error);
1021+
}finally{
1022+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
10181023
}
10191024
}
10201025
}
@@ -1632,7 +1637,9 @@ function fulfillReference(
16321637
constelement: any=handler.value;
16331638
switch(key){
16341639
case'3':
1635-
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1640+
if(__DEV__){
1641+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1642+
}
16361643
element.props=mappedValue;
16371644
break;
16381645
case'4':
@@ -1648,7 +1655,9 @@ function fulfillReference(
16481655
}
16491656
break;
16501657
default:
1651-
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
1658+
if (__DEV__) {
1659+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1660+
}
16521661
break;
16531662
}
16541663
}elseif(__DEV__&&!reference.isDebug){
@@ -2086,7 +2095,7 @@ function getOutlinedModel<T>(
20862095
response,
20872096
map,
20882097
path.slice(i-1),
2089-
false,
2098+
isInitializingDebugInfo,
20902099
);
20912100
}
20922101
case HALTED: {
@@ -2158,14 +2167,21 @@ function getOutlinedModel<T>(
21582167
}
21592168

21602169
const chunkValue = map(response, value, parentObject, key);
2161-
if (
2162-
parentObject[0] === REACT_ELEMENT_TYPE &&
2163-
(key=== '4' ||key=== '5')
2164-
){
2165-
// If we're resolving the "owner" or "stack" slot of an Element array, we don't call
2166-
// transferReferencedDebugInfo because this reference is to a debug chunk.
2167-
} else {
2168-
transferReferencedDebugInfo(initializingChunk,chunk);
2170+
if (__DEV__) {
2171+
if(
2172+
parentObject[0]===REACT_ELEMENT_TYPE&&
2173+
(key==='4'||key==='5')
2174+
){
2175+
// If we're resolving the "owner" or "stack" slot of an Element array,
2176+
// we don't call transferReferencedDebugInfo because this reference is
2177+
// to a debug chunk.
2178+
} else if (isInitializingDebugInfo) {
2179+
// If we're resolving references as part of debug info resolution, we
2180+
// don't call transferReferencedDebugInfo because these references are
2181+
// to debug chunks.
2182+
} else {
2183+
transferReferencedDebugInfo(initializingChunk,chunk);
2184+
}
21692185
}
21702186
returnchunkValue;
21712187
casePENDING:
@@ -2177,7 +2193,7 @@ function getOutlinedModel<T>(
21772193
response,
21782194
map,
21792195
path,
2180-
false,
2196+
isInitializingDebugInfo,
21812197
);
21822198
caseHALTED: {
21832199
// Add a dependency that will never resolve.
@@ -4264,15 +4280,21 @@ function resolveIOInfo(
42644280
): void{
42654281
constchunks=response._chunks;
42664282
letchunk=chunks.get(id);
4267-
if(!chunk){
4268-
chunk =createResolvedModelChunk(response,model);
4269-
chunks.set(id,chunk);
4270-
initializeModelChunk(chunk);
4271-
}else{
4272-
resolveModelChunk(response,chunk,model);
4273-
if(chunk.status===RESOLVED_MODEL){
4283+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
4284+
isInitializingDebugInfo=true;
4285+
try{
4286+
if(!chunk){
4287+
chunk=createResolvedModelChunk(response,model);
4288+
chunks.set(id,chunk);
42744289
initializeModelChunk(chunk);
4290+
}else{
4291+
resolveModelChunk(response,chunk,model);
4292+
if(chunk.status===RESOLVED_MODEL){
4293+
initializeModelChunk(chunk);
4294+
}
42754295
}
4296+
} finally {
4297+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
42764298
}
42774299
if (chunk.status === INITIALIZED) {
42784300
initializeIOInfo(response,chunk.value);

‎packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3633,4 +3633,40 @@ describe('ReactFlightAsyncDebugInfo', () => {
36333633
`);
36343634
}
36353635
});
3636+
3637+
it('should not exponentially accumulate debug info on outlined debug chunks',async()=>{
3638+
// Regression test: Each Level wraps its received `context` prop in a new
3639+
// object before passing it down. This creates props deduplication
3640+
// references to the parent's outlined chunk alongside the owner reference,
3641+
// giving 2 references per level to the direct parent's chunk. Without
3642+
// skipping transferReferencedDebugInfo during debug info resolution, this
3643+
// test would fail with an infinite loop detection error.
3644+
asyncfunctionLevel({depth, context}){
3645+
awaitdelay(0);
3646+
if(depth===0){
3647+
return<div>Hello, World!</div>;
3648+
}
3649+
constnewContext={prev: context,id: depth};
3650+
returnReactServer.createElement(Level,{
3651+
depth: depth-1,
3652+
context: newContext,
3653+
});
3654+
}
3655+
3656+
conststream=ReactServerDOMServer.renderToPipeableStream(
3657+
ReactServer.createElement(Level,{depth: 20,context: {root: true}}),
3658+
);
3659+
3660+
constreadable=newStream.PassThrough(streamOptions);
3661+
constresult=ReactServerDOMClient.createFromNodeStream(readable,{
3662+
moduleMap: {},
3663+
moduleLoading: {},
3664+
});
3665+
stream.pipe(readable);
3666+
3667+
constresolved=awaitresult;
3668+
expect(resolved.type).toBe('div');
3669+
3670+
awaitfinishLoadingStream(readable);
3671+
});
36363672
});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Commit 47d1ad1

Browse files
authored
[Flight] Skip transferReferencedDebugInfo during debug info resolution (#35795)
When the Flight Client resolves chunk references during model parsing, it calls `transferReferencedDebugInfo` to propagate debug info entries from referenced chunks to the parent chunk. Debug info on chunks is later moved to their resolved values, where it is used by React DevTools to show performance tracks and what a component was suspended by. Debug chunks themselves (specifically `ReactComponentInfo`, `ReactAsyncInfo`, `ReactIOInfo`, and their outlined references) are metadata that is never rendered. They don't need debug info attached to them. Without this fix, debug info entries accumulate on outlined debug chunks via their references to other debug chunks (e.g. owner chains and props deduplication paths). Since each outlined chunk's accumulated entries are copied to every chunk that references it, this creates exponential growth in deep component trees, which can cause the dev server to hang and run out of memory. This generalizes the existing skip of `transferReferencedDebugInfo` for Element owner/stack references (which already recognizes that references to debug chunks don't need debug info transferred) to all references resolved during debug info resolution. It adds an `isInitializingDebugInfo` flag set in `initializeDebugChunk` and `resolveIOInfo`, which propagates through all nested `initializeModelChunk` calls within the same synchronous stack. For the async path, `waitForReference` captures the flag at call time into `InitializationReference.isDebug`, so deferred fulfillments also skip the transfer.
1 parent e8c6362 commit 47d1ad1

2 files changed

Lines changed: 77 additions & 19 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,7 @@ type InitializationHandler = {
943943
};
944944
let initializingHandler: null | InitializationHandler = null;
945945
let initializingChunk: null | BlockedChunk<any> = null;
946+
let isInitializingDebugInfo: boolean = false;
946947

947948
function initializeDebugChunk(
948949
response: Response,
@@ -951,6 +952,8 @@ function initializeDebugChunk(
951952
constdebugChunk=chunk._debugChunk;
952953
if(debugChunk!==null){
953954
const debugInfo =chunk._debugInfo;
955+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
956+
isInitializingDebugInfo=true;
954957
try{
955958
if(debugChunk.status===RESOLVED_MODEL){
956959
// Find the index of this debug info by walking the linked list.
@@ -1015,6 +1018,8 @@ function initializeDebugChunk(
10151018
}
10161019
}catch(error){
10171020
triggerErrorOnChunk(response,chunk,error);
1021+
}finally{
1022+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
10181023
}
10191024
}
10201025
}
@@ -1632,7 +1637,9 @@ function fulfillReference(
16321637
constelement: any=handler.value;
16331638
switch(key){
16341639
case'3':
1635-
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1640+
if(__DEV__){
1641+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1642+
}
16361643
element.props=mappedValue;
16371644
break;
16381645
case'4':
@@ -1648,7 +1655,9 @@ function fulfillReference(
16481655
}
16491656
break;
16501657
default:
1651-
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
1658+
if (__DEV__) {
1659+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1660+
}
16521661
break;
16531662
}
16541663
}elseif(__DEV__&&!reference.isDebug){
@@ -2086,7 +2095,7 @@ function getOutlinedModel<T>(
20862095
response,
20872096
map,
20882097
path.slice(i-1),
2089-
false,
2098+
isInitializingDebugInfo,
20902099
);
20912100
}
20922101
case HALTED: {
@@ -2158,14 +2167,21 @@ function getOutlinedModel<T>(
21582167
}
21592168

21602169
const chunkValue = map(response, value, parentObject, key);
2161-
if (
2162-
parentObject[0] === REACT_ELEMENT_TYPE &&
2163-
(key=== '4' ||key=== '5')
2164-
){
2165-
// If we're resolving the "owner" or "stack" slot of an Element array, we don't call
2166-
// transferReferencedDebugInfo because this reference is to a debug chunk.
2167-
} else {
2168-
transferReferencedDebugInfo(initializingChunk,chunk);
2170+
if (__DEV__) {
2171+
if(
2172+
parentObject[0]===REACT_ELEMENT_TYPE&&
2173+
(key==='4'||key==='5')
2174+
){
2175+
// If we're resolving the "owner" or "stack" slot of an Element array,
2176+
// we don't call transferReferencedDebugInfo because this reference is
2177+
// to a debug chunk.
2178+
} else if (isInitializingDebugInfo) {
2179+
// If we're resolving references as part of debug info resolution, we
2180+
// don't call transferReferencedDebugInfo because these references are
2181+
// to debug chunks.
2182+
} else {
2183+
transferReferencedDebugInfo(initializingChunk,chunk);
2184+
}
21692185
}
21702186
returnchunkValue;
21712187
casePENDING:
@@ -2177,7 +2193,7 @@ function getOutlinedModel<T>(
21772193
response,
21782194
map,
21792195
path,
2180-
false,
2196+
isInitializingDebugInfo,
21812197
);
21822198
caseHALTED: {
21832199
// Add a dependency that will never resolve.
@@ -4264,15 +4280,21 @@ function resolveIOInfo(
42644280
): void{
42654281
constchunks=response._chunks;
42664282
letchunk=chunks.get(id);
4267-
if(!chunk){
4268-
chunk =createResolvedModelChunk(response,model);
4269-
chunks.set(id,chunk);
4270-
initializeModelChunk(chunk);
4271-
}else{
4272-
resolveModelChunk(response,chunk,model);
4273-
if(chunk.status===RESOLVED_MODEL){
4283+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
4284+
isInitializingDebugInfo=true;
4285+
try{
4286+
if(!chunk){
4287+
chunk=createResolvedModelChunk(response,model);
4288+
chunks.set(id,chunk);
42744289
initializeModelChunk(chunk);
4290+
}else{
4291+
resolveModelChunk(response,chunk,model);
4292+
if(chunk.status===RESOLVED_MODEL){
4293+
initializeModelChunk(chunk);
4294+
}
42754295
}
4296+
} finally {
4297+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
42764298
}
42774299
if (chunk.status === INITIALIZED) {
42784300
initializeIOInfo(response,chunk.value);

‎packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3633,4 +3633,40 @@ describe('ReactFlightAsyncDebugInfo', () => {
36333633
`);
36343634
}
36353635
});
3636+
3637+
it('should not exponentially accumulate debug info on outlined debug chunks',async()=>{
3638+
// Regression test: Each Level wraps its received `context` prop in a new
3639+
// object before passing it down. This creates props deduplication
3640+
// references to the parent's outlined chunk alongside the owner reference,
3641+
// giving 2 references per level to the direct parent's chunk. Without
3642+
// skipping transferReferencedDebugInfo during debug info resolution, this
3643+
// test would fail with an infinite loop detection error.
3644+
asyncfunctionLevel({depth, context}){
3645+
awaitdelay(0);
3646+
if(depth===0){
3647+
return<div>Hello, World!</div>;
3648+
}
3649+
constnewContext={prev: context,id: depth};
3650+
returnReactServer.createElement(Level,{
3651+
depth: depth-1,
3652+
context: newContext,
3653+
});
3654+
}
3655+
3656+
conststream=ReactServerDOMServer.renderToPipeableStream(
3657+
ReactServer.createElement(Level,{depth: 20,context: {root: true}}),
3658+
);
3659+
3660+
constreadable=newStream.PassThrough(streamOptions);
3661+
constresult=ReactServerDOMClient.createFromNodeStream(readable,{
3662+
moduleMap: {},
3663+
moduleLoading: {},
3664+
});
3665+
stream.pipe(readable);
3666+
3667+
constresolved=awaitresult;
3668+
expect(resolved.type).toBe('div');
3669+
3670+
awaitfinishLoadingStream(readable);
3671+
});
36363672
});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 47d1ad1

Browse files
authored
[Flight] Skip transferReferencedDebugInfo during debug info resolution (#35795)
When the Flight Client resolves chunk references during model parsing, it calls `transferReferencedDebugInfo` to propagate debug info entries from referenced chunks to the parent chunk. Debug info on chunks is later moved to their resolved values, where it is used by React DevTools to show performance tracks and what a component was suspended by. Debug chunks themselves (specifically `ReactComponentInfo`, `ReactAsyncInfo`, `ReactIOInfo`, and their outlined references) are metadata that is never rendered. They don't need debug info attached to them. Without this fix, debug info entries accumulate on outlined debug chunks via their references to other debug chunks (e.g. owner chains and props deduplication paths). Since each outlined chunk's accumulated entries are copied to every chunk that references it, this creates exponential growth in deep component trees, which can cause the dev server to hang and run out of memory. This generalizes the existing skip of `transferReferencedDebugInfo` for Element owner/stack references (which already recognizes that references to debug chunks don't need debug info transferred) to all references resolved during debug info resolution. It adds an `isInitializingDebugInfo` flag set in `initializeDebugChunk` and `resolveIOInfo`, which propagates through all nested `initializeModelChunk` calls within the same synchronous stack. For the async path, `waitForReference` captures the flag at call time into `InitializationReference.isDebug`, so deferred fulfillments also skip the transfer.
1 parent e8c6362 commit 47d1ad1

2 files changed

Lines changed: 77 additions & 19 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,7 @@ type InitializationHandler = {
943943
};
944944
let initializingHandler: null | InitializationHandler = null;
945945
let initializingChunk: null | BlockedChunk<any> = null;
946+
let isInitializingDebugInfo: boolean = false;
946947

947948
function initializeDebugChunk(
948949
response: Response,
@@ -951,6 +952,8 @@ function initializeDebugChunk(
951952
constdebugChunk=chunk._debugChunk;
952953
if(debugChunk!==null){
953954
const debugInfo =chunk._debugInfo;
955+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
956+
isInitializingDebugInfo=true;
954957
try{
955958
if(debugChunk.status===RESOLVED_MODEL){
956959
// Find the index of this debug info by walking the linked list.
@@ -1015,6 +1018,8 @@ function initializeDebugChunk(
10151018
}
10161019
}catch(error){
10171020
triggerErrorOnChunk(response,chunk,error);
1021+
}finally{
1022+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
10181023
}
10191024
}
10201025
}
@@ -1632,7 +1637,9 @@ function fulfillReference(
16321637
constelement: any=handler.value;
16331638
switch(key){
16341639
case'3':
1635-
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1640+
if(__DEV__){
1641+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1642+
}
16361643
element.props=mappedValue;
16371644
break;
16381645
case'4':
@@ -1648,7 +1655,9 @@ function fulfillReference(
16481655
}
16491656
break;
16501657
default:
1651-
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
1658+
if (__DEV__) {
1659+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1660+
}
16521661
break;
16531662
}
16541663
}elseif(__DEV__&&!reference.isDebug){
@@ -2086,7 +2095,7 @@ function getOutlinedModel<T>(
20862095
response,
20872096
map,
20882097
path.slice(i-1),
2089-
false,
2098+
isInitializingDebugInfo,
20902099
);
20912100
}
20922101
case HALTED: {
@@ -2158,14 +2167,21 @@ function getOutlinedModel<T>(
21582167
}
21592168

21602169
const chunkValue = map(response, value, parentObject, key);
2161-
if (
2162-
parentObject[0] === REACT_ELEMENT_TYPE &&
2163-
(key=== '4' ||key=== '5')
2164-
){
2165-
// If we're resolving the "owner" or "stack" slot of an Element array, we don't call
2166-
// transferReferencedDebugInfo because this reference is to a debug chunk.
2167-
} else {
2168-
transferReferencedDebugInfo(initializingChunk,chunk);
2170+
if (__DEV__) {
2171+
if(
2172+
parentObject[0]===REACT_ELEMENT_TYPE&&
2173+
(key==='4'||key==='5')
2174+
){
2175+
// If we're resolving the "owner" or "stack" slot of an Element array,
2176+
// we don't call transferReferencedDebugInfo because this reference is
2177+
// to a debug chunk.
2178+
} else if (isInitializingDebugInfo) {
2179+
// If we're resolving references as part of debug info resolution, we
2180+
// don't call transferReferencedDebugInfo because these references are
2181+
// to debug chunks.
2182+
} else {
2183+
transferReferencedDebugInfo(initializingChunk,chunk);
2184+
}
21692185
}
21702186
returnchunkValue;
21712187
casePENDING:
@@ -2177,7 +2193,7 @@ function getOutlinedModel<T>(
21772193
response,
21782194
map,
21792195
path,
2180-
false,
2196+
isInitializingDebugInfo,
21812197
);
21822198
caseHALTED: {
21832199
// Add a dependency that will never resolve.
@@ -4264,15 +4280,21 @@ function resolveIOInfo(
42644280
): void{
42654281
constchunks=response._chunks;
42664282
letchunk=chunks.get(id);
4267-
if(!chunk){
4268-
chunk =createResolvedModelChunk(response,model);
4269-
chunks.set(id,chunk);
4270-
initializeModelChunk(chunk);
4271-
}else{
4272-
resolveModelChunk(response,chunk,model);
4273-
if(chunk.status===RESOLVED_MODEL){
4283+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
4284+
isInitializingDebugInfo=true;
4285+
try{
4286+
if(!chunk){
4287+
chunk=createResolvedModelChunk(response,model);
4288+
chunks.set(id,chunk);
42744289
initializeModelChunk(chunk);
4290+
}else{
4291+
resolveModelChunk(response,chunk,model);
4292+
if(chunk.status===RESOLVED_MODEL){
4293+
initializeModelChunk(chunk);
4294+
}
42754295
}
4296+
} finally {
4297+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
42764298
}
42774299
if (chunk.status === INITIALIZED) {
42784300
initializeIOInfo(response,chunk.value);

‎packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3633,4 +3633,40 @@ describe('ReactFlightAsyncDebugInfo', () => {
36333633
`);
36343634
}
36353635
});
3636+
3637+
it('should not exponentially accumulate debug info on outlined debug chunks',async()=>{
3638+
// Regression test: Each Level wraps its received `context` prop in a new
3639+
// object before passing it down. This creates props deduplication
3640+
// references to the parent's outlined chunk alongside the owner reference,
3641+
// giving 2 references per level to the direct parent's chunk. Without
3642+
// skipping transferReferencedDebugInfo during debug info resolution, this
3643+
// test would fail with an infinite loop detection error.
3644+
asyncfunctionLevel({depth, context}){
3645+
awaitdelay(0);
3646+
if(depth===0){
3647+
return<div>Hello, World!</div>;
3648+
}
3649+
constnewContext={prev: context,id: depth};
3650+
returnReactServer.createElement(Level,{
3651+
depth: depth-1,
3652+
context: newContext,
3653+
});
3654+
}
3655+
3656+
conststream=ReactServerDOMServer.renderToPipeableStream(
3657+
ReactServer.createElement(Level,{depth: 20,context: {root: true}}),
3658+
);
3659+
3660+
constreadable=newStream.PassThrough(streamOptions);
3661+
constresult=ReactServerDOMClient.createFromNodeStream(readable,{
3662+
moduleMap: {},
3663+
moduleLoading: {},
3664+
});
3665+
stream.pipe(readable);
3666+
3667+
constresolved=awaitresult;
3668+
expect(resolved.type).toBe('div');
3669+
3670+
awaitfinishLoadingStream(readable);
3671+
});
36363672
});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 47d1ad1

Browse files
authored
[Flight] Skip transferReferencedDebugInfo during debug info resolution (#35795)
When the Flight Client resolves chunk references during model parsing, it calls `transferReferencedDebugInfo` to propagate debug info entries from referenced chunks to the parent chunk. Debug info on chunks is later moved to their resolved values, where it is used by React DevTools to show performance tracks and what a component was suspended by. Debug chunks themselves (specifically `ReactComponentInfo`, `ReactAsyncInfo`, `ReactIOInfo`, and their outlined references) are metadata that is never rendered. They don't need debug info attached to them. Without this fix, debug info entries accumulate on outlined debug chunks via their references to other debug chunks (e.g. owner chains and props deduplication paths). Since each outlined chunk's accumulated entries are copied to every chunk that references it, this creates exponential growth in deep component trees, which can cause the dev server to hang and run out of memory. This generalizes the existing skip of `transferReferencedDebugInfo` for Element owner/stack references (which already recognizes that references to debug chunks don't need debug info transferred) to all references resolved during debug info resolution. It adds an `isInitializingDebugInfo` flag set in `initializeDebugChunk` and `resolveIOInfo`, which propagates through all nested `initializeModelChunk` calls within the same synchronous stack. For the async path, `waitForReference` captures the flag at call time into `InitializationReference.isDebug`, so deferred fulfillments also skip the transfer.
1 parent e8c6362 commit 47d1ad1

2 files changed

Lines changed: 77 additions & 19 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,7 @@ type InitializationHandler = {
943943
};
944944
let initializingHandler: null | InitializationHandler = null;
945945
let initializingChunk: null | BlockedChunk<any> = null;
946+
let isInitializingDebugInfo: boolean = false;
946947

947948
function initializeDebugChunk(
948949
response: Response,
@@ -951,6 +952,8 @@ function initializeDebugChunk(
951952
constdebugChunk=chunk._debugChunk;
952953
if(debugChunk!==null){
953954
const debugInfo =chunk._debugInfo;
955+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
956+
isInitializingDebugInfo=true;
954957
try{
955958
if(debugChunk.status===RESOLVED_MODEL){
956959
// Find the index of this debug info by walking the linked list.
@@ -1015,6 +1018,8 @@ function initializeDebugChunk(
10151018
}
10161019
}catch(error){
10171020
triggerErrorOnChunk(response,chunk,error);
1021+
}finally{
1022+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
10181023
}
10191024
}
10201025
}
@@ -1632,7 +1637,9 @@ function fulfillReference(
16321637
constelement: any=handler.value;
16331638
switch(key){
16341639
case'3':
1635-
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1640+
if(__DEV__){
1641+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1642+
}
16361643
element.props=mappedValue;
16371644
break;
16381645
case'4':
@@ -1648,7 +1655,9 @@ function fulfillReference(
16481655
}
16491656
break;
16501657
default:
1651-
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
1658+
if (__DEV__) {
1659+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1660+
}
16521661
break;
16531662
}
16541663
}elseif(__DEV__&&!reference.isDebug){
@@ -2086,7 +2095,7 @@ function getOutlinedModel<T>(
20862095
response,
20872096
map,
20882097
path.slice(i-1),
2089-
false,
2098+
isInitializingDebugInfo,
20902099
);
20912100
}
20922101
case HALTED: {
@@ -2158,14 +2167,21 @@ function getOutlinedModel<T>(
21582167
}
21592168

21602169
const chunkValue = map(response, value, parentObject, key);
2161-
if (
2162-
parentObject[0] === REACT_ELEMENT_TYPE &&
2163-
(key=== '4' ||key=== '5')
2164-
){
2165-
// If we're resolving the "owner" or "stack" slot of an Element array, we don't call
2166-
// transferReferencedDebugInfo because this reference is to a debug chunk.
2167-
} else {
2168-
transferReferencedDebugInfo(initializingChunk,chunk);
2170+
if (__DEV__) {
2171+
if(
2172+
parentObject[0]===REACT_ELEMENT_TYPE&&
2173+
(key==='4'||key==='5')
2174+
){
2175+
// If we're resolving the "owner" or "stack" slot of an Element array,
2176+
// we don't call transferReferencedDebugInfo because this reference is
2177+
// to a debug chunk.
2178+
} else if (isInitializingDebugInfo) {
2179+
// If we're resolving references as part of debug info resolution, we
2180+
// don't call transferReferencedDebugInfo because these references are
2181+
// to debug chunks.
2182+
} else {
2183+
transferReferencedDebugInfo(initializingChunk,chunk);
2184+
}
21692185
}
21702186
returnchunkValue;
21712187
casePENDING:
@@ -2177,7 +2193,7 @@ function getOutlinedModel<T>(
21772193
response,
21782194
map,
21792195
path,
2180-
false,
2196+
isInitializingDebugInfo,
21812197
);
21822198
caseHALTED: {
21832199
// Add a dependency that will never resolve.
@@ -4264,15 +4280,21 @@ function resolveIOInfo(
42644280
): void{
42654281
constchunks=response._chunks;
42664282
letchunk=chunks.get(id);
4267-
if(!chunk){
4268-
chunk =createResolvedModelChunk(response,model);
4269-
chunks.set(id,chunk);
4270-
initializeModelChunk(chunk);
4271-
}else{
4272-
resolveModelChunk(response,chunk,model);
4273-
if(chunk.status===RESOLVED_MODEL){
4283+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
4284+
isInitializingDebugInfo=true;
4285+
try{
4286+
if(!chunk){
4287+
chunk=createResolvedModelChunk(response,model);
4288+
chunks.set(id,chunk);
42744289
initializeModelChunk(chunk);
4290+
}else{
4291+
resolveModelChunk(response,chunk,model);
4292+
if(chunk.status===RESOLVED_MODEL){
4293+
initializeModelChunk(chunk);
4294+
}
42754295
}
4296+
} finally {
4297+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
42764298
}
42774299
if (chunk.status === INITIALIZED) {
42784300
initializeIOInfo(response,chunk.value);

‎packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3633,4 +3633,40 @@ describe('ReactFlightAsyncDebugInfo', () => {
36333633
`);
36343634
}
36353635
});
3636+
3637+
it('should not exponentially accumulate debug info on outlined debug chunks',async()=>{
3638+
// Regression test: Each Level wraps its received `context` prop in a new
3639+
// object before passing it down. This creates props deduplication
3640+
// references to the parent's outlined chunk alongside the owner reference,
3641+
// giving 2 references per level to the direct parent's chunk. Without
3642+
// skipping transferReferencedDebugInfo during debug info resolution, this
3643+
// test would fail with an infinite loop detection error.
3644+
asyncfunctionLevel({depth, context}){
3645+
awaitdelay(0);
3646+
if(depth===0){
3647+
return<div>Hello, World!</div>;
3648+
}
3649+
constnewContext={prev: context,id: depth};
3650+
returnReactServer.createElement(Level,{
3651+
depth: depth-1,
3652+
context: newContext,
3653+
});
3654+
}
3655+
3656+
conststream=ReactServerDOMServer.renderToPipeableStream(
3657+
ReactServer.createElement(Level,{depth: 20,context: {root: true}}),
3658+
);
3659+
3660+
constreadable=newStream.PassThrough(streamOptions);
3661+
constresult=ReactServerDOMClient.createFromNodeStream(readable,{
3662+
moduleMap: {},
3663+
moduleLoading: {},
3664+
});
3665+
stream.pipe(readable);
3666+
3667+
constresolved=awaitresult;
3668+
expect(resolved.type).toBe('div');
3669+
3670+
awaitfinishLoadingStream(readable);
3671+
});
36363672
});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Commit 47d1ad1

Browse files
authored
[Flight] Skip transferReferencedDebugInfo during debug info resolution (#35795)
When the Flight Client resolves chunk references during model parsing, it calls `transferReferencedDebugInfo` to propagate debug info entries from referenced chunks to the parent chunk. Debug info on chunks is later moved to their resolved values, where it is used by React DevTools to show performance tracks and what a component was suspended by. Debug chunks themselves (specifically `ReactComponentInfo`, `ReactAsyncInfo`, `ReactIOInfo`, and their outlined references) are metadata that is never rendered. They don't need debug info attached to them. Without this fix, debug info entries accumulate on outlined debug chunks via their references to other debug chunks (e.g. owner chains and props deduplication paths). Since each outlined chunk's accumulated entries are copied to every chunk that references it, this creates exponential growth in deep component trees, which can cause the dev server to hang and run out of memory. This generalizes the existing skip of `transferReferencedDebugInfo` for Element owner/stack references (which already recognizes that references to debug chunks don't need debug info transferred) to all references resolved during debug info resolution. It adds an `isInitializingDebugInfo` flag set in `initializeDebugChunk` and `resolveIOInfo`, which propagates through all nested `initializeModelChunk` calls within the same synchronous stack. For the async path, `waitForReference` captures the flag at call time into `InitializationReference.isDebug`, so deferred fulfillments also skip the transfer.
1 parent e8c6362 commit 47d1ad1

2 files changed

Lines changed: 77 additions & 19 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,7 @@ type InitializationHandler = {
943943
};
944944
let initializingHandler: null | InitializationHandler = null;
945945
let initializingChunk: null | BlockedChunk<any> = null;
946+
let isInitializingDebugInfo: boolean = false;
946947

947948
function initializeDebugChunk(
948949
response: Response,
@@ -951,6 +952,8 @@ function initializeDebugChunk(
951952
constdebugChunk=chunk._debugChunk;
952953
if(debugChunk!==null){
953954
const debugInfo =chunk._debugInfo;
955+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
956+
isInitializingDebugInfo=true;
954957
try{
955958
if(debugChunk.status===RESOLVED_MODEL){
956959
// Find the index of this debug info by walking the linked list.
@@ -1015,6 +1018,8 @@ function initializeDebugChunk(
10151018
}
10161019
}catch(error){
10171020
triggerErrorOnChunk(response,chunk,error);
1021+
}finally{
1022+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
10181023
}
10191024
}
10201025
}
@@ -1632,7 +1637,9 @@ function fulfillReference(
16321637
constelement: any=handler.value;
16331638
switch(key){
16341639
case'3':
1635-
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1640+
if(__DEV__){
1641+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1642+
}
16361643
element.props=mappedValue;
16371644
break;
16381645
case'4':
@@ -1648,7 +1655,9 @@ function fulfillReference(
16481655
}
16491656
break;
16501657
default:
1651-
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
1658+
if (__DEV__) {
1659+
transferReferencedDebugInfo(handler.chunk,fulfilledChunk);
1660+
}
16521661
break;
16531662
}
16541663
}elseif(__DEV__&&!reference.isDebug){
@@ -2086,7 +2095,7 @@ function getOutlinedModel<T>(
20862095
response,
20872096
map,
20882097
path.slice(i-1),
2089-
false,
2098+
isInitializingDebugInfo,
20902099
);
20912100
}
20922101
case HALTED: {
@@ -2158,14 +2167,21 @@ function getOutlinedModel<T>(
21582167
}
21592168

21602169
const chunkValue = map(response, value, parentObject, key);
2161-
if (
2162-
parentObject[0] === REACT_ELEMENT_TYPE &&
2163-
(key=== '4' ||key=== '5')
2164-
){
2165-
// If we're resolving the "owner" or "stack" slot of an Element array, we don't call
2166-
// transferReferencedDebugInfo because this reference is to a debug chunk.
2167-
} else {
2168-
transferReferencedDebugInfo(initializingChunk,chunk);
2170+
if (__DEV__) {
2171+
if(
2172+
parentObject[0]===REACT_ELEMENT_TYPE&&
2173+
(key==='4'||key==='5')
2174+
){
2175+
// If we're resolving the "owner" or "stack" slot of an Element array,
2176+
// we don't call transferReferencedDebugInfo because this reference is
2177+
// to a debug chunk.
2178+
} else if (isInitializingDebugInfo) {
2179+
// If we're resolving references as part of debug info resolution, we
2180+
// don't call transferReferencedDebugInfo because these references are
2181+
// to debug chunks.
2182+
} else {
2183+
transferReferencedDebugInfo(initializingChunk,chunk);
2184+
}
21692185
}
21702186
returnchunkValue;
21712187
casePENDING:
@@ -2177,7 +2193,7 @@ function getOutlinedModel<T>(
21772193
response,
21782194
map,
21792195
path,
2180-
false,
2196+
isInitializingDebugInfo,
21812197
);
21822198
caseHALTED: {
21832199
// Add a dependency that will never resolve.
@@ -4264,15 +4280,21 @@ function resolveIOInfo(
42644280
): void{
42654281
constchunks=response._chunks;
42664282
letchunk=chunks.get(id);
4267-
if(!chunk){
4268-
chunk =createResolvedModelChunk(response,model);
4269-
chunks.set(id,chunk);
4270-
initializeModelChunk(chunk);
4271-
}else{
4272-
resolveModelChunk(response,chunk,model);
4273-
if(chunk.status===RESOLVED_MODEL){
4283+
constprevIsInitializingDebugInfo=isInitializingDebugInfo;
4284+
isInitializingDebugInfo=true;
4285+
try{
4286+
if(!chunk){
4287+
chunk=createResolvedModelChunk(response,model);
4288+
chunks.set(id,chunk);
42744289
initializeModelChunk(chunk);
4290+
}else{
4291+
resolveModelChunk(response,chunk,model);
4292+
if(chunk.status===RESOLVED_MODEL){
4293+
initializeModelChunk(chunk);
4294+
}
42754295
}
4296+
} finally {
4297+
isInitializingDebugInfo=prevIsInitializingDebugInfo;
42764298
}
42774299
if (chunk.status === INITIALIZED) {
42784300
initializeIOInfo(response,chunk.value);

‎packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3633,4 +3633,40 @@ describe('ReactFlightAsyncDebugInfo', () => {
36333633
`);
36343634
}
36353635
});
3636+
3637+
it('should not exponentially accumulate debug info on outlined debug chunks',async()=>{
3638+
// Regression test: Each Level wraps its received `context` prop in a new
3639+
// object before passing it down. This creates props deduplication
3640+
// references to the parent's outlined chunk alongside the owner reference,
3641+
// giving 2 references per level to the direct parent's chunk. Without
3642+
// skipping transferReferencedDebugInfo during debug info resolution, this
3643+
// test would fail with an infinite loop detection error.
3644+
asyncfunctionLevel({depth, context}){
3645+
awaitdelay(0);
3646+
if(depth===0){
3647+
return<div>Hello, World!</div>;
3648+
}
3649+
constnewContext={prev: context,id: depth};
3650+
returnReactServer.createElement(Level,{
3651+
depth: depth-1,
3652+
context: newContext,
3653+
});
3654+
}
3655+
3656+
conststream=ReactServerDOMServer.renderToPipeableStream(
3657+
ReactServer.createElement(Level,{depth: 20,context: {root: true}}),
3658+
);
3659+
3660+
constreadable=newStream.PassThrough(streamOptions);
3661+
constresult=ReactServerDOMClient.createFromNodeStream(readable,{
3662+
moduleMap: {},
3663+
moduleLoading: {},
3664+
});
3665+
stream.pipe(readable);
3666+
3667+
constresolved=awaitresult;
3668+
expect(resolved.type).toBe('div');
3669+
3670+
awaitfinishLoadingStream(readable);
3671+
});
36363672
});

0 commit comments

Comments
 (0)