@samitouri / QOS-React-2 / commits / e60063d9e7

[Fizz] Include a component stack in prod but only lazily generate it (#30132)

When we added component stacks to Fizz in prod it severely slowed down common cases where you intentionally are throwing error for purposes of client rendering. Our parent component stack generation is very slow since call components with fake errors to generate them. Therefore we disabled them in prod but included them in prerenders. https://github.com/facebook/react/pull/27850 However, we still kept generating data structures for them and the code still exists there for the prerenders. We could stop generating the data structures which are not completely free but also not crazy bad. What we can do instead is just lazily generate the component stacks. This is in fact what plain stacks do anyway. This doesn't work as well in Fiber because the data structures are live but on the server they're immutable so it's fine to do it later as well. That way you can choose to not read this getter for intentionally thrown errors - after inspecting the Error object - yet still get component stacks in prod for other errors.

Sebastian Markbåge committed Jul 2, 2024 at 16:05 UTC e60063d9e7d346e92a5af42975a2fe7dd306f86f
1 file changed +25 -29
packages/react-server/src/ReactFizzServer.js
+25 -29
@@ -908,27 +908,23 @@ type ThrownInfo = {
908 export type ErrorInfo = ThrownInfo;
909 export type PostponeInfo = ThrownInfo;
910
911 -// While we track component stacks in prod all the time we only produce a reified stack in dev and
912 -// during prerender in Prod. The reason for this is that the stack is useful for prerender where the timeliness
913 -// of the request is less critical than the observability of the execution. For renders and resumes however we
914 -// prioritize speed of the request.
915 -function getThrownInfo(
916 - request: Request,
917 - node: null | ComponentStackNode,
918 -): ThrownInfo {
919 - if (
920 - node &&
921 - // Always produce a stack in dev
922 - (__DEV__ ||
923 - // Produce a stack in prod if we're in a prerender
924 - request.trackedPostpones !== null)
925 - ) {
926 - return {
927 - componentStack: getStackFromNode(node),
928 - };
929 - } else {
930 - return {};
911 +function getThrownInfo(node: null | ComponentStackNode): ThrownInfo {
912 + const errorInfo: ThrownInfo = {};
913 + if (node) {
914 + Object.defineProperty(errorInfo, 'componentStack', {
915 + configurable: true,
916 + enumerable: true,
917 + get() {
918 + // Lazyily generate the stack since it's expensive.
919 + const stack = getStackFromNode(node);
920 + Object.defineProperty(errorInfo, 'componentStack', {
921 + value: stack,
922 + });
923 + return stack;
924 + },
925 + });
926 }
927 + return errorInfo;
928 }
929
930 function encodeErrorForBoundary(
@@ -1127,7 +1123,7 @@ function renderSuspenseBoundary(
1123 } catch (error: mixed) {
1124 contentRootSegment.status = ERRORED;
1125 newBoundary.status = CLIENT_RENDERED;
1130 - const thrownInfo = getThrownInfo(request, task.componentStack);
1126 + const thrownInfo = getThrownInfo(task.componentStack);
1127 let errorDigest;
1128 if (
1129 enablePostpone &&
@@ -1273,7 +1269,7 @@ function replaySuspenseBoundary(
1269 }
1270 } catch (error: mixed) {
1271 resumedBoundary.status = CLIENT_RENDERED;
1276 - const thrownInfo = getThrownInfo(request, task.componentStack);
1272 + const thrownInfo = getThrownInfo(task.componentStack);
1273 let errorDigest;
1274 if (
1275 enablePostpone &&
@@ -2337,7 +2333,7 @@ function replayElement(
2333 // in the original prerender. What's unable to complete is the child
2334 // replay nodes which might be Suspense boundaries which are able to
2335 // absorb the error and we can still continue with siblings.
2340 - const thrownInfo = getThrownInfo(request, task.componentStack);
2336 + const thrownInfo = getThrownInfo(task.componentStack);
2337 erroredReplay(
2338 request,
2339 task.blockedBoundary,
@@ -2868,7 +2864,7 @@ function replayFragment(
2864 // replay nodes which might be Suspense boundaries which are able to
2865 // absorb the error and we can still continue with siblings.
2866 // This is an error, stash the component stack if it is null.
2871 - const thrownInfo = getThrownInfo(request, task.componentStack);
2867 + const thrownInfo = getThrownInfo(task.componentStack);
2868 erroredReplay(
2869 request,
2870 task.blockedBoundary,
@@ -3467,7 +3463,7 @@ function renderNode(
3463 const trackedPostpones = request.trackedPostpones;
3464
3465 const postponeInstance: Postpone = (x: any);
3470 - const thrownInfo = getThrownInfo(request, task.componentStack);
3466 + const thrownInfo = getThrownInfo(task.componentStack);
3467 const postponedSegment = injectPostponedHole(
3468 request,
3469 ((task: any): RenderTask), // We don't use ReplayTasks in prerenders.
@@ -3782,7 +3778,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
3778 boundary.status = CLIENT_RENDERED;
3779 // We construct an errorInfo from the boundary's componentStack so the error in dev will indicate which
3780 // boundary the message is referring to
3785 - const errorInfo = getThrownInfo(request, task.componentStack);
3781 + const errorInfo = getThrownInfo(task.componentStack);
3782 let errorDigest;
3783 if (
3784 enablePostpone &&
@@ -4074,7 +4070,7 @@ function retryRenderTask(
4070 task.abortSet.delete(task);
4071 const postponeInstance: Postpone = (x: any);
4072
4077 - const postponeInfo = getThrownInfo(request, task.componentStack);
4073 + const postponeInfo = getThrownInfo(task.componentStack);
4074 logPostpone(request, postponeInstance.message, postponeInfo);
4075 trackPostpone(request, trackedPostpones, task, segment);
4076 finishedTask(request, task.blockedBoundary, segment);
@@ -4082,7 +4078,7 @@ function retryRenderTask(
4078 }
4079 }
4080
4085 - const errorInfo = getThrownInfo(request, task.componentStack);
4081 + const errorInfo = getThrownInfo(task.componentStack);
4082 task.abortSet.delete(task);
4083 segment.status = ERRORED;
4084 erroredTask(request, task.blockedBoundary, x, errorInfo);
@@ -4156,7 +4152,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void {
4152 }
4153 task.replay.pendingTasks--;
4154 task.abortSet.delete(task);
4159 - const errorInfo = getThrownInfo(request, task.componentStack);
4155 + const errorInfo = getThrownInfo(task.componentStack);
4156 erroredReplay(
4157 request,
4158 task.blockedBoundary,