[Flight] Create a fast path parseStackTrace which skips generating a string stack (#33735)
When we know that the object that we pass in is immediately parsed, then we know it couldn't have been reified into a unstructured stack yet. In this path we assume that we'll trigger `Error.prepareStackTrace`. Since we know that nobody else will read the stack after us, we can skip generating a string stack and just return empty. We can also skip caching.
Sebastian Markbåge committed
Jul 9, 2025 at 09:06 UTC
3a43e72d665af98b62d9c3c54bd288597dd27e6e
3 files changed
+46
-14
packages/react-server/src/ReactFlightServer.js
+2
-1
@@ -94,6 +94,7 @@ import {
94
getCurrentAsyncSequence,
95
getAsyncSequenceFromPromise,
96
parseStackTrace,
97
+ parseStackTracePrivate,
98
supportsComponentStorage,
99
componentStorage,
100
unbadgeConsole,
@@ -316,7 +317,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
317
// one stack frame but keeping it simple for now and include all frames.
318
const stack = filterStackTrace(
319
request,
319
- parseStackTrace(new Error('react-stack-top-frame'), 1),
320
+ parseStackTracePrivate(new Error('react-stack-top-frame'), 1) || [],
321
);
322
request.pendingDebugChunks++;
323
const owner: null | ReactComponentInfo = resolveOwner();
packages/react-server/src/ReactFlightServerConfigDebugNode.js
+9
-6
@@ -29,7 +29,7 @@ import {resolveOwner} from './flight/ReactFlightCurrentOwner';
29
import {resolveRequest, isAwaitInUserspace} from './ReactFlightServer';
30
import {createHook, executionAsyncId, AsyncResource} from 'async_hooks';
31
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
32
-import {parseStackTrace} from './ReactFlightServerConfig';
32
+import {parseStackTracePrivate} from './ReactFlightServerConfig';
33
34
// $FlowFixMe[method-unbinding]
35
const getAsyncId = AsyncResource.prototype.asyncId;
@@ -129,8 +129,8 @@ export function initAsyncDebugInfo(): void {
129
if (request === null) {
130
// We don't collect stacks for awaits that weren't in the scope of a specific render.
131
} else {
132
- stack = parseStackTrace(new Error(), 5);
133
- if (!isAwaitInUserspace(request, stack)) {
132
+ stack = parseStackTracePrivate(new Error(), 5);
133
+ if (stack !== null && !isAwaitInUserspace(request, stack)) {
134
// If this await was not done directly in user space, then clear the stack. We won't use it
135
// anyway. This lets future awaits on this await know that we still need to get their stacks
136
// until we find one in user space.
@@ -153,7 +153,8 @@ export function initAsyncDebugInfo(): void {
153
node = ({
154
tag: UNRESOLVED_PROMISE_NODE,
155
owner: owner,
156
- stack: owner === null ? null : parseStackTrace(new Error(), 5),
156
+ stack:
157
+ owner === null ? null : parseStackTracePrivate(new Error(), 5),
158
start: performance.now(),
159
end: -1.1, // Set when we resolve.
160
promise: new WeakRef((resource: Promise<any>)),
@@ -175,7 +176,8 @@ export function initAsyncDebugInfo(): void {
176
node = ({
177
tag: IO_NODE,
178
owner: owner,
178
- stack: owner === null ? parseStackTrace(new Error(), 3) : null,
179
+ stack:
180
+ owner === null ? parseStackTracePrivate(new Error(), 3) : null,
181
start: performance.now(),
182
end: -1.1, // Only set when pinged.
183
promise: null,
@@ -191,7 +193,8 @@ export function initAsyncDebugInfo(): void {
193
node = ({
194
tag: IO_NODE,
195
owner: owner,
194
- stack: owner === null ? parseStackTrace(new Error(), 3) : null,
196
+ stack:
197
+ owner === null ? parseStackTracePrivate(new Error(), 3) : null,
198
start: performance.now(),
199
end: -1.1, // Only set when pinged.
200
promise: null,
packages/react-server/src/ReactFlightStackConfigV8.js
+35
-7
@@ -49,7 +49,7 @@ function getMethodCallName(callSite: CallSite): string {
49
return result;
50
}
51
52
-function collectStackTrace(
52
+function collectStackTracePrivate(
53
error: Error,
54
structuredStackTrace: CallSite[],
55
): string {
@@ -79,11 +79,11 @@ function collectStackTrace(
79
let filename = callSite.getScriptNameOrSourceURL() || '<anonymous>';
80
if (filename === '<anonymous>') {
81
filename = '';
82
- }
83
- if (callSite.isEval() && !filename) {
84
- const origin = callSite.getEvalOrigin();
85
- if (origin) {
86
- filename = origin.toString() + ', <anonymous>';
82
+ if (callSite.isEval()) {
83
+ const origin = callSite.getEvalOrigin();
84
+ if (origin) {
85
+ filename = origin.toString() + ', <anonymous>';
86
+ }
87
}
88
}
89
const line = callSite.getLineNumber() || 0;
@@ -101,6 +101,15 @@ function collectStackTrace(
101
result.push([name, filename, line, col, enclosingLine, enclosingCol]);
102
}
103
}
104
+ collectedStackTrace = result;
105
+ return '';
106
+}
107
+
108
+function collectStackTrace(
109
+ error: Error,
110
+ structuredStackTrace: CallSite[],
111
+): string {
112
+ collectStackTracePrivate(error, structuredStackTrace);
113
// At the same time we generate a string stack trace just in case someone
114
// else reads it. Ideally, we'd call the previous prepareStackTrace to
115
// ensure it's in the expected format but it's common for that to be
@@ -115,7 +124,6 @@ function collectStackTrace(
124
for (let i = 0; i < structuredStackTrace.length; i++) {
125
stack += '\n at ' + structuredStackTrace[i].toString();
126
}
118
- collectedStackTrace = result;
127
return stack;
128
}
129
@@ -131,6 +139,26 @@ const stackTraceCache: WeakMap<Error, ReactStackTrace> = __DEV__
139
? new WeakMap()
140
: (null: any);
141
142
+// This version is only used when React fully owns the Error object and there's no risk of it having
143
+// been already initialized and no risky that anyone else will initialize it later.
144
+export function parseStackTracePrivate(
145
+ error: Error,
146
+ skipFrames: number,
147
+): null | ReactStackTrace {
148
+ collectedStackTrace = null;
149
+ framesToSkip = skipFrames;
150
+ const previousPrepare = Error.prepareStackTrace;
151
+ Error.prepareStackTrace = collectStackTracePrivate;
152
+ try {
153
+ if (error.stack !== '') {
154
+ return null;
155
+ }
156
+ } finally {
157
+ Error.prepareStackTrace = previousPrepare;
158
+ }
159
+ return collectedStackTrace;
160
+}
161
+
162
export function parseStackTrace(
163
error: Error,
164
skipFrames: number,