Skip to content

Investigating a Severe Performance Regression in Node.js v22 and v24 #60719

Description

@titanism

Investigating a Severe Performance Regression in Node.js v22 and v24

Hello everyone,

We (@forwardemail) have been running some benchmarks with better-sqlite3-multiple-ciphers and came across what appears to be a significant performance regression in newer Node.js versions. I wanted to share my findings in case they are helpful to the community, especially for those who rely on native modules with SQLite. We use encrypted SQLite databases for storing folks email, see our write-up at https://forwardemail.net/en/blog/docs/best-quantum-safe-encrypted-email-service.

Summary of Findings

My benchmarks indicate that Node.js v24 is approximately 57% slower than v20 for SQLite SELECT operations, while Node.js v22 shows a smaller regression of about 7-9%. These findings suggest the performance issue may not be specific to better-sqlite3 but could be related to a broader change in the V8 engine that affects native modules.

Benchmark Results

I've created a benchmark suite to reproduce these results, which you can find here: https://github.com/forwardemail/sqlite-benchmarks

Here is a summary of the results I observed with better-sqlite3-multiple-ciphers:

Node.js Version SELECT ops/sec vs v20 LTS
v20.19.5 (LTS) 18,383 baseline
v22.21.1 ~17,000 -7%
v24.11.1 ~7,900 -57%

As the table shows, the performance on v24 is less than half of that on v20 for this specific database workload.

Potential Root Cause

After some investigation, I believe this regression may be linked to the V8 13.6 engine upgrade in Node.js v24. The V8 versions for each Node.js release are as follows:

  • Node.js v20: V8 11.3 (Stable Performance)
  • Node.js v22: V8 12.4 (Minor Regression)
  • Node.js v24: V8 13.6 (Major Regression)

The most significant change in V8 13.6 appears to be the move to a V8-owned CppHeap (nodejs/node#52718), which alters how C++ objects are managed and garbage collected in native modules. While this change was intended to simplify the API, it seems to have had an unintended impact on the performance of native modules that frequently cross the JS/C++ boundary.

How This Might Affect SQLite Performance

The performance-critical paths in better-sqlite3 involve frequent C++ object allocation, heavy interaction between JS and C++, and string conversions. The new V8-owned CppHeap may have introduced different garbage collection timing, allocation patterns, or boundary-crossing overhead that could be contributing to this slowdown.

Related Upstream Issues

This issue may be part of a broader pattern of performance regressions in recent Node.js versions:

  1. Startup regression: nodejs/performance#180
  2. Module loading regression: nodejs/node#60397
  3. CppHeap deprecation: nodejs/node#52718

These issues suggest that V8 11.3 (in Node.js v20) may have been a more optimized version for native module performance.

Suggestions for a Path Forward

I've put together some thoughts on how we might be able to approach this. I'm happy to help with any of these steps.

For Users Experiencing This Issue

For those affected by this performance regression, a possible short-term workaround could be to remain on Node.js v20 LTS. It appears to be the most stable and performant version for this workload and is supported until April 2026.

Potential Areas for Investigation

It seems the performance degradation might be linked to the CppHeap changes in V8. It could be beneficial for the V8 and Node.js teams to investigate this further. Some areas that might be worth exploring include:

  • Benchmarking the CppHeap regression: A performance comparison of native modules before and after the CppHeap changes could help confirm the impact.
  • Profiling hot paths: It might be useful to profile the following areas:
    • Object allocation in the V8-owned CppHeap
    • Garbage collection behavior for frequently created C++ objects
    • Overhead from JS/C++ boundary crossings
    • String encoding/decoding with WriteUtf8V2()

Possible Long-Term Solutions

If the regression in V8 cannot be addressed upstream, it might be necessary for native modules like better-sqlite3 to adapt. Some potential strategies could include:

  • Object pooling: Reusing statement objects to reduce allocation overhead.
  • Batch operations: Processing multiple rows at once to minimize boundary crossings.
  • Optimized string handling: Caching string conversions or exploring alternative V8 APIs.
  • Manual memory management: Taking more explicit control over the object lifecycle to mitigate GC issues.

However, it seems that addressing this at the V8 level would be the most effective solution, as it would benefit all native modules.

Reproduction

To reproduce the benchmark results, you can clone the repository and run the tests:

git clone https://github.com/forwardemail/sqlite-benchmarks.git
cd sqlite-benchmarks
npm install

# Test with different Node.js versions
nvm use 20 && npm run benchmark
nvm use 22 && npm run benchmark
nvm use 24 && npm run benchmark

The results have been consistent on both macOS ARM and Linux x64.

Questions for the Community

  1. Has anyone else encountered similar performance issues with v24?
  2. Are there any V8 flags or Node.js options that might help mitigate this?
  3. Would it be appropriate to escalate this to the V8 team for their input?

I am more than willing to assist with further profiling, testing, or providing any additional information that might be helpful. Thank you for your time and consideration.

Activity

  1. mcollina commented on Nov 14, 2025

    @mcollina
  2. changed the title [-]Severe performance regression in Node.js v22 and v24 (57% slower on v24)[/-] [+]Investigating a Severe Performance Regression in Node.js v22 and v24[/+] on Nov 14, 2025
  3. added
    performanceIssues and PRs related to the performance of Node.js.
    on Nov 14, 2025
  4. joyeecheung commented on Nov 16, 2025

    @joyeecheung
    Member

    I think it’s unlikely to be related to CppHeap, because currently CppHeap in Node.js is mostly only used for script compilation; unless the application is heavy on compiling scripts, which seems unlikely judging from the name of the application, it’s likely something else.

  5. lovell commented on Nov 18, 2025

    @lovell
    Contributor

    I've been able to reproduce this with the farmhash native module using its benchmark tests. The performance when transferring a JavaScript String to a C++ std::string (via Utf8Value) has approximately halved between v22.21.1 and v24.11.1. Hopefully this information can help someone narrow down possible underlying changes/causes.

    Node.js v22.21.1

    Using key of length 10000
    farmhash-hash32-string x 335,871 ops/sec ±0.93% (92 runs sampled)
    farmhash-hash32-buffer x 723,035 ops/sec ±0.49% (99 runs sampled)
    farmhash-hash32+seed-string x 335,389 ops/sec ±0.84% (91 runs sampled)
    farmhash-hash32+seed-buffer x 678,827 ops/sec ±1.13% (89 runs sampled)
    farmhash-hash64-string x 593,499 ops/sec ±0.98% (99 runs sampled)
    farmhash-hash64-buffer x 2,123,900 ops/sec ±0.85% (92 runs sampled)
    farmhash-hash64+seed-string x 522,131 ops/sec ±0.38% (96 runs sampled)
    farmhash-hash64+seed-buffer x 1,784,170 ops/sec ±0.31% (93 runs sampled)
    farmhash-hash64+seeds-string x 517,844 ops/sec ±0.52% (100 runs sampled)
    farmhash-hash64+seeds-buffer x 1,746,578 ops/sec ±0.60% (93 runs sampled)
    

    Node.js v24.11.1

    Using key of length 10000
    farmhash-hash32-string x 160,187 ops/sec ±1.93% (94 runs sampled)
    farmhash-hash32-buffer x 725,678 ops/sec ±1.21% (97 runs sampled)
    farmhash-hash32+seed-string x 165,089 ops/sec ±0.44% (95 runs sampled)
    farmhash-hash32+seed-buffer x 723,313 ops/sec ±0.21% (100 runs sampled)
    farmhash-hash64-string x 195,270 ops/sec ±0.44% (94 runs sampled)
    farmhash-hash64-buffer x 2,176,829 ops/sec ±0.20% (94 runs sampled)
    farmhash-hash64+seed-string x 194,099 ops/sec ±0.18% (102 runs sampled)
    farmhash-hash64+seed-buffer x 1,798,656 ops/sec ±0.19% (95 runs sampled)
    farmhash-hash64+seeds-string x 193,943 ops/sec ±0.30% (102 runs sampled)
    farmhash-hash64+seeds-buffer x 1,762,318 ops/sec ±0.60% (96 runs sampled)
    

    The hash32 functions return a Number, the hash64 functions return a BigInt and seed is a Number. The .node binary was compiled once against Node-API v9 and used with both versions of Node.js.

  6. joyeecheung commented on Nov 20, 2025

    @joyeecheung
    Member

    I suspect you are looking at the regression fixed by https://chromium-review.googlesource.com/c/v8/v8/+/7124103

  7. jasnell commented on Nov 20, 2025

    @jasnell
    Member

    Agree with @joyeecheung ... even the differences in the benchmark numbers there align with what we were seeing in that regression. The regression has been fixed upstream and other improvements are being made. We'll pick it up either when v8 is updated to a version including that patch or when someone cherry picks it in.

  8. lovell commented on Nov 20, 2025

    @lovell
    Contributor

    Brilliant, thank you very much.

  9. Dhruv-Garg79 commented on Dec 1, 2025

    @Dhruv-Garg79

    Will this fix land in v24?

  10. liamjay commented on Dec 18, 2025

    @liamjay

    Will this be fixed by the EOL of Node.js v20?

  11. Dhruv-Garg79 commented on Feb 12, 2026

    @Dhruv-Garg79

    @jasnell I think the fix has already been merged upstream, but v24 will not update to a new v8 version.
    So the only choice is to cherry-pick this change, right? I can try doing that. I have gone through the contributing.md, is there anything else I should keep in mind?

  12. richardlau commented on Feb 12, 2026

    @richardlau
    Member

    @jasnell I think the fix has already been merged upstream, but v24 will not update to a new v8 version. So the only choice is to cherry-pick this change, right? I can try doing that. I have gone through the contributing.md, is there anything else I should keep in mind?

    For V8 cherry-picks: https://github.com/nodejs/node/blob/main/doc/contributing/maintaining/maintaining-V8.md

  13. Dhruv-Garg79 commented on Feb 13, 2026

    @Dhruv-Garg79

    @richardlau I have added the PR - #61798
    Let me know if I need to change anything (I took reference from other v8 backport PRs for naming/messages).

    Edit:

    I ran the same benchmarks as mentioned by @lovell using local build of my branch

    Using key of length 10000
    farmhash-hash32-string x 321,806 ops/sec ±1.83% (96 runs sampled)
    farmhash-hash32-buffer x 660,698 ops/sec ±0.48% (98 runs sampled)
    farmhash-hash32+seed-string x 322,186 ops/sec ±0.30% (97 runs sampled)
    farmhash-hash32+seed-buffer x 656,473 ops/sec ±0.42% (97 runs sampled)
    farmhash-fingerprint32-string x 320,854 ops/sec ±0.73% (98 runs sampled)
    farmhash-fingerprint32-buffer x 664,482 ops/sec ±0.21% (98 runs sampled)
    farmhash-fingerprint64-string x 488,863 ops/sec ±0.56% (99 runs sampled)
    farmhash-fingerprint64-buffer x 1,946,991 ops/sec ±0.40% (95 runs sampled)
    farmhash-hash64-string x 506,420 ops/sec ±0.21% (96 runs sampled)
    farmhash-hash64-buffer x 1,943,824 ops/sec ±1.59% (98 runs sampled)
    farmhash-hash64+seed-string x 488,255 ops/sec ±0.50% (98 runs sampled)
    farmhash-hash64+seed-buffer x 1,925,802 ops/sec ±0.19% (99 runs sampled)
    farmhash-hash64+seeds-string x 477,885 ops/sec ±0.87% (98 runs sampled)
    farmhash-hash64+seeds-buffer x 1,911,134 ops/sec ±0.19% (99 runs sampled)
    

    compared to v22.21.1

    Using key of length 10000
    farmhash-hash32-string x 458,830 ops/sec ±0.18% (102 runs sampled)
    farmhash-hash32-buffer x 658,979 ops/sec ±0.39% (99 runs sampled)
    farmhash-hash32+seed-string x 453,676 ops/sec ±0.58% (101 runs sampled)
    farmhash-hash32+seed-buffer x 656,388 ops/sec ±0.20% (98 runs sampled)
    farmhash-fingerprint32-string x 453,784 ops/sec ±1.21% (94 runs sampled)
    farmhash-fingerprint32-buffer x 660,733 ops/sec ±0.18% (102 runs sampled)
    farmhash-fingerprint64-string x 850,938 ops/sec ±0.18% (101 runs sampled)
    farmhash-fingerprint64-buffer x 1,976,712 ops/sec ±0.27% (96 runs sampled)
    farmhash-hash64-string x 848,459 ops/sec ±0.55% (97 runs sampled)
    farmhash-hash64-buffer x 1,996,060 ops/sec ±1.09% (98 runs sampled)
    farmhash-hash64+seed-string x 847,551 ops/sec ±0.19% (100 runs sampled)
    farmhash-hash64+seed-buffer x 1,936,307 ops/sec ±1.33% (98 runs sampled)
    farmhash-hash64+seeds-string x 844,907 ops/sec ±0.23% (95 runs sampled)
    farmhash-hash64+seeds-buffer x 1,923,725 ops/sec ±0.50% (99 runs sampled)
    

    compared to v24.12.0

    Using key of length 10000
    farmhash-hash32-string x 140,004 ops/sec ±0.92% (99 runs sampled)
    farmhash-hash32-buffer x 663,064 ops/sec ±0.18% (102 runs sampled)
    farmhash-hash32+seed-string x 140,667 ops/sec ±0.17% (101 runs sampled)
    farmhash-hash32+seed-buffer x 651,224 ops/sec ±1.28% (98 runs sampled)
    farmhash-fingerprint32-string x 140,524 ops/sec ±0.20% (97 runs sampled)
    farmhash-fingerprint32-buffer x 655,123 ops/sec ±1.23% (92 runs sampled)
    farmhash-fingerprint64-string x 161,221 ops/sec ±0.46% (100 runs sampled)
    farmhash-fingerprint64-buffer x 1,714,939 ops/sec ±3.84% (88 runs sampled)
    farmhash-hash64-string x 143,886 ops/sec ±5.92% (84 runs sampled)
    farmhash-hash64-buffer x 1,812,083 ops/sec ±1.66% (85 runs sampled)
    farmhash-hash64+seed-string x 160,367 ops/sec ±0.42% (95 runs sampled)
    farmhash-hash64+seed-buffer x 1,800,484 ops/sec ±1.96% (93 runs sampled)
    farmhash-hash64+seeds-string x 157,284 ops/sec ±0.67% (88 runs sampled)
    farmhash-hash64+seeds-buffer x 1,901,620 ops/sec ±0.84% (98 runs sampled)
    

    It's much better than v24.12.0, but still slower compared to v22.21.1

  14. targos commented on Feb 13, 2026

    @targos
    Member

    #61712 landed on main and will make its way into releases.

  15. ChALkeR commented on Feb 15, 2026

    @ChALkeR
    Member

    @Dhruv-Garg79 how is v24.13.1?

  16. Dhruv-Garg79 commented on Feb 16, 2026

    @Dhruv-Garg79

    @ChALkeR it's almost same as v24.12.0.

    v24.13.1

    Using key of length 10000
    farmhash-hash32-string x 141,619 ops/sec ±0.06% (101 runs sampled)
    farmhash-hash32-buffer x 664,420 ops/sec ±0.26% (100 runs sampled)
    farmhash-hash32+seed-string x 141,348 ops/sec ±0.06% (100 runs sampled)
    farmhash-hash32+seed-buffer x 661,005 ops/sec ±0.07% (99 runs sampled)
    farmhash-fingerprint32-string x 141,057 ops/sec ±0.28% (101 runs sampled)
    farmhash-fingerprint32-buffer x 656,560 ops/sec ±1.49% (99 runs sampled)
    farmhash-fingerprint64-string x 162,925 ops/sec ±0.88% (99 runs sampled)
    farmhash-fingerprint64-buffer x 1,957,725 ops/sec ±0.22% (98 runs sampled)
    farmhash-hash64-string x 164,242 ops/sec ±0.05% (100 runs sampled)
    farmhash-hash64-buffer x 1,986,741 ops/sec ±0.07% (103 runs sampled)
    farmhash-hash64+seed-string x 163,889 ops/sec ±0.08% (101 runs sampled)
    farmhash-hash64+seed-buffer x 1,901,639 ops/sec ±1.82% (96 runs sampled)
    farmhash-hash64+seeds-string x 163,722 ops/sec ±0.07% (100 runs sampled)
    farmhash-hash64+seeds-buffer x 1,927,029 ops/sec ±0.08% (102 runs sampled)
    
  17. github-actions commented on Jul 20, 2026

    @github-actions
    Contributor

    This issue has been marked as stale due to 90 days of inactivity.
    It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.

  18. added
    staleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.
    on Jul 20, 2026
  19. lovell commented on Jul 20, 2026

    @lovell
    Contributor

    I retested and can confirm JS to C++ string conversion is ~2x faster using Node.js 26 compared with Node.js 22 (and therefore ~4x faster than Node.js 24).

    24.18.0

    Using key of length 10000
    farmhash-hash32-string x 152,300 ops/sec ±2.82% (92 runs sampled)
    farmhash-hash32-buffer x 735,850 ops/sec ±0.20% (92 runs sampled)
    farmhash-hash32+seed-string x 166,747 ops/sec ±0.17% (97 runs sampled)
    farmhash-hash32+seed-buffer x 722,352 ops/sec ±0.18% (92 runs sampled)
    farmhash-hash64-string x 196,910 ops/sec ±0.34% (95 runs sampled)
    farmhash-hash64-buffer x 2,197,063 ops/sec ±0.25% (97 runs sampled)
    farmhash-hash64+seed-string x 192,814 ops/sec ±0.43% (92 runs sampled)
    farmhash-hash64+seed-buffer x 1,801,434 ops/sec ±0.26% (91 runs sampled)
    farmhash-hash64+seeds-string x 193,432 ops/sec ±0.18% (98 runs sampled)
    farmhash-hash64+seeds-buffer x 1,798,926 ops/sec ±0.29% (94 runs sampled)
    

    26.5.0

    Using key of length 10000
    farmhash-hash32-string x 599,876 ops/sec ±0.40% (99 runs sampled)
    farmhash-hash32-buffer x 732,521 ops/sec ±0.13% (99 runs sampled)
    farmhash-hash32+seed-string x 588,945 ops/sec ±0.55% (92 runs sampled)
    farmhash-hash32+seed-buffer x 723,193 ops/sec ±0.13% (98 runs sampled)
    farmhash-hash64-string x 1,327,109 ops/sec ±0.32% (97 runs sampled)
    farmhash-hash64-buffer x 2,177,610 ops/sec ±0.42% (97 runs sampled)
    farmhash-hash64+seed-string x 1,171,964 ops/sec ±0.20% (95 runs sampled)
    farmhash-hash64+seed-buffer x 1,784,501 ops/sec ±0.50% (96 runs sampled)
    farmhash-hash64+seeds-string x 1,162,053 ops/sec ±0.19% (99 runs sampled)
    farmhash-hash64+seeds-buffer x 1,708,044 ops/sec ±0.74% (97 runs sampled)
    

    Thanks everyone for investigating/fixing/improving.

  20. removed
    staleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.
    on Jul 21, 2026
  21. added
    sqliteIssues and PRs related to the SQLite subsystem.
    on Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceIssues and PRs related to the performance of Node.js.sqliteIssues and PRs related to the SQLite subsystem.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions