Backport v8 "Reduce stack frame summarization costs" patch for React development speedup
Chưa có ai nhận issue này.
- #65764 của @aduh95 — đã đóng, không merge
- Ngôn ngữ chính
- JavaScript
- Star
- 122k
- Fork
- 37.3k
- Merge trung bình
- 4 ngày 2 giờ
- Pull request đã merge (30 ngày)
- 283
Mô tả
Summary
Please backport V8 commit https://github.com/v8/v8/commit/c9c0abfa51f06ea3c87c4ee9bb519fa8b01fcbbf, “Reduce stack frame summarization costs,” to the Node.js 24 LTS release line.
The backport should also include the required follow-up fix https://github.com/v8/v8/commit/1a0089053443ae4001fd15b84f1b46f330a0b3fa, which falls back to the full frame walk when a receiver is unboxed.
The optimization substantially reduces the synchronous cost of constructing Error objects with stack capture enabled. This cost is paid even before .stack is read because V8 eagerly collects the raw stack-frame information at construction time; only conversion to the formatted stack string is lazy.
React use case
There are two paths in React that heavily rely on Error() in dev.
React element creation
React constructs Error objects in the development implementations of jsx, jsxDEV, and createElement.
Every JSX expression is compiled into one of these element-creation calls. For example:
function App() {
return (
<Layout>
<Header />
<Content />
</Layout>
);
}
Each element in this tree can cause React to capture an Error:
const debugStack = Error('react-stack-top-frame');
The error is not thrown. React stores it as the element’s development-only _debugStack field together with its _owner:
element._debugStack = debugStack;
element._owner = currentOwner;
When the element becomes a Fiber, these fields are copied to fiber._debugStack and fiber._debugOwner. React can later format the captured error to determine the source location where the owner created that element.
This is used for:
- Owner stacks attached to development warnings and errors.
React.captureOwnerStack().- Component source information displayed by React DevTools.
- Key and invalid-child warnings.
- Server-rendering and RSC debug information.
React uses sentinel frames such as react-stack-top-frame and react_stack_bottom_frame to remove the JSX runtime and other React internals when the error is eventually formatted. The resulting owner stack points back to the application component that created the element.
The capture occurs in several development element-creation paths:
jsxDEV, used by the development JSX transform.- The development implementations of
jsxandjsxs, including dependencies compiled with the production JSX signature but loaded in a development React build. createElement, used by the classic JSX transform, manualReact.createElementcalls, and some libraries.
Because JSX element creation is fundamental to rendering, this is a very hot path. A component tree may create hundreds or thousands of elements, and the captures happen again when components re-render.
React rate-limits detailed owner-stack collection to 10,000 recently created elements. After that it reuses a shared “unknown owner” stack until the counter is periodically reset. Consequently, a development workload can still perform up to 10,000 real Error() captures during each reset period.
This path already temporarily caps Error.stackTraceLimit at 10 to avoid inheriting a much higher application-configured value. However, the V8 optimization remains valuable: in the Node.js 24 benchmark, the patch made Error() construction 2.17× faster even at a stack limit of 10.
These captures are development-only, but development performance is important for server rendering, tests, local application startup, and update responsiveness. Improving V8’s eager stack capture directly reduces the overhead of React’s JSX and createElement hot paths without changing React’s diagnostics.
Promise Tracking
In development, React uses Node.js’s async_hooks API to collect debugging information for asynchronous component execution.
React installs an init hook and handles PROMISE resources. When a relevant Promise is initialized, React constructs an Error and parses its stack to record where the Promise or await originated. This information is associated with the current Server Component owner and later used to produce async debug information and component-owner stacks.
Conceptually, the hot path contains:
createHook({
init(asyncId, type) {
if (type === 'PROMISE') {
const error = new Error();
const stack = parseStackTrace(error);
// Associate the stack with the Promise and component owner.
}
},
});
This callback runs synchronously for every Promise created while the RSC instrumentation is active. Promise-heavy renders can therefore execute this path thousands or tens of thousands of times.
The cost also depends on the ambient value of Error.stackTraceLimit. Although V8 defaults it to 10, applications and development tooling sometimes raise it to values such as 50.
- The patched V8 is over twice as fast even at the default limit of 10.
- Other frameworks, diagnostics libraries, tracing systems, and user code have similar
Error()capture paths.
React immediately parses the captured stack in the real path. The benchmark below deliberately leaves .stack unread during timing to isolate the eager construction work improved by this V8 patch.
Reproduction
Tested with Node.js v24.18.1 at a recursive JavaScript depth of 64:
Benchmark code
'use strict';
const inspector = require('node:inspector');
const {performance} = require('node:perf_hooks');
const limit = Number(process.argv[2]);
if (limit !== 10 && limit !== 50) {
throw new Error('Expected a stack trace limit of 10 or 50');
}
const iterations = 100_000;
const samples = [];
let sink;
Error.stackTraceLimit = limit;
function captureAtDepth(depth) {
if (depth === 0) {
sink = Error();
return;
}
captureAtDepth(depth - 1);
}
function measure() {
const start = performance.now();
for (let i = 0; i < iterations; i++) {
captureAtDepth(64);
}
return performance.now() - start;
}
// Warm up before collecting samples.
for (let i = 0; i < 20_000; i++) {
captureAtDepth(64);
}
for (let i = 0; i < 5; i++) {
samples.push(measure());
}
samples.sort((a, b) => a - b);
console.log(
JSON.stringify(
{
node: process.version,
v8: process.versions.v8,
limit,
iterations,
samples,
median: samples[Math.floor(samples.length / 2)],
// Read only after timing to verify the number of captured frames.
capturedLines: sink.stack.split('\n').length,
inspectorAttached: inspector.url() !== undefined,
execArgv: process.execArgv,
nodeOptions: process.env.NODE_OPTIONS,
},
null,
2,
),
);
Run it in fresh processes with no inspector or NODE_OPTIONS as that affects performance:
env -u NODE_OPTIONS node error-construction.js 10
env -u NODE_OPTIONS node error-construction.js 50
I ran two passes in opposite order, producing 10 timing samples for each configuration.
Results
Both binaries are Node.js v24.18.1 on the same machine and use the same V8 13.6 base. The only V8 version difference is the local backport suffix:
| Node.js v24.18.1 | Limit 10 | Limit 50 | Limit 50 / 10 |
|---|---|---|---|
Unpatched, V8 13.6.233.17-node.50 |
376.5 ms | 1,539.2 ms | 4.09× |
Patched, V8 13.6.233.17-node.51 |
173.5 ms | 589.9 ms | 3.40× |
| Improvement | 2.17× | 2.61× |
The marginal cost per additional captured frame between limits 10 and 50 falls from approximately 291 ns to 104 ns, a 2.79× improvement.
The stack was not read during the timed section. It was read afterward to verify that the captures contained 11 and 51 lines respectively. The inspector was not attached, process.execArgv was empty, and NODE_OPTIONS was unset.
Expected outcome
It would be highly beneficial for Node.js 24 to incorporate the upstream V8 optimization so that stack-frame collection does less work while preserving existing Error and stack-trace behavior.
The change has been present upstream since April 2026 and has a material effect on framework development instrumentation, error capture, tracing, and other workloads that construct large numbers of Error objects.
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Hướng nghiên cứu
Xem xét các commit V8 c9c0abfa51f06ea3c87c4ee9bb519fa8b01fcbbf và 1a0089053443ae4001fd15b84f1b46f330a0b3fa trong bối cảnh nhánh phát hành Node.js 24 LTS. Chạy benchmark error-construction.js được cung cấp với các giới hạn stack là 10 và 50, sau đó xác minh rằng backport cải thiện chi phí khởi tạo trong khi vẫn giữ nguyên số lượng frame được ghi lại và hành vi stack trace hiện có.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- javascript, nodejs
- Lĩnh vực
- backend, performance
- Loại issue
- Tính năng
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức độ hoạt động
- Sôi nổi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức phù hợp với người mới
- 35/100