Fixing V8 OOM Crashes in Electron: Debugging, Memory Analysis, and Building a Custom Electron

This article walks through diagnosing V8FatalErrorCallback OOM crashes in an Electron‑based PC Taobao Live client, explores stack analysis, disables compilation cache, adjusts V8 heap limits, compiles a custom Electron without pointer compression, and uses Chrome DevTools Memory and Performance tools along with runtime monitoring to locate and fix the underlying memory leak.

Alipay Experience Technology
Alipay Experience Technology
Alipay Experience Technology
Fixing V8 OOM Crashes in Electron: Debugging, Memory Analysis, and Building a Custom Electron

Background

After months of crash mitigation, the new Electron‑based PC Taobao Live streaming client finally surpassed the stability of the old Qt version. However, about 40% of remaining crashes are memory OOM, especially V8FatalErrorCallback OOM errors that persisted for over a month.

Analyzing the V8FatalErrorCallback Crash

The stack trace shows that the V8 old‑generation heap runs out of memory during GC, triggering the fatal callback.

Disabling Compilation Cache

Removing the require('v8-compile-cache') module and launching Electron with the flag --no-compilation-cache eliminates the cache‑related memory growth:

app.commandLine.appendSwitch('js-flags', '--no-compilation-cache')

Increasing V8 Heap Limits

Attempts to raise the V8 heap by setting --max-semi-space-size=64 and later --max-old-space-size=8192 only partially mitigated the issue.

app.commandLine.appendSwitch('js-flags', '--max-semi-space-size=64')
app.commandLine.appendSwitch('js-flags', '--max-old-space-size=8192')

Understanding V8 Heap Limits

V8 calculates the heap limit as 3 * max_semi_space_size + max_old_generation_size. The defaults are 8 MiB (or 16 MiB on 64‑bit) for the semi‑space and 700 MiB (or 1400 MiB) for the old generation, yielding a total of ~724 MiB (32‑bit) or ~1448 MiB (64‑bit). On a 16 GiB machine, V8 may still cap the heap at 4 GiB due to pointer compression.

Key source files: src/third_party/electron_node/deps/v8/include/v8.h – defines

heap_size_limit()
src/third_party/electron_node/deps/v8/src/heap/heap.cc

– implements MaxReserved() and the heap size formula src/third_party/electron_node/deps/v8/src/api/api.cc – sets the limits based on physical memory

Disabling V8 Pointer Compression

Pointer compression reduces memory usage but limits the heap to 4 GiB on 64‑bit. To lift this limit, disable compression in the build configuration.

# In src/electron/build/args/all.gn
v8_enable_pointer_compression = false
v8_enable_pointer_compression_shared_cage = false
# In src/third_party/electron_node/deps/v8/BUILD.gn
v8_enable_pointer_compression = false
v8_enable_pointer_compression_shared_cage = false

Re‑generate GN files and rebuild:

cd src
set CHROMIUM_BUILDTOOLS_PATH=%cd%\buildtools
gn gen out/Release_disable_v8_pointer_compression --args="import(\"//electron/build/args/release.gn\")"
ninja -C out/Release_disable_v8_pointer_compression electron -j 4

After rebuilding, performance.memory.heapSizeLimit reports an 8 GiB limit on a 64‑bit system.

Using Chrome DevTools to Find the Real Leak

Even with a larger heap, the application still leaked memory. The Memory panel showed a growing String count, but the source was not obvious. The Performance panel, with the Memory checkbox enabled, recorded a timeline where the JS heap grew from 24 MiB to 38 MiB.

By clicking a spike in the timeline, the offending code was identified as the electron-log error handler that repeatedly logged failed arms fetch requests:

import TraceSdk from '@ali/trace-sdk-node'
import log from 'electron-log'
let trace = TraceSdk()
const sendErrorLog = trace.logError
log.catchErrors({
  onError(error) {
    sendErrorLog(error)
  },
})

The log captured errors like:

Unhandled Exception FetchError: request to https://s-gm.mmstat.com/arms.1.1 failed, reason: getaddrinfo ENOTFOUND s-gm.mmstat.com

When the network was disconnected, these errors kept being generated, causing the memory leak.

Fixing the Leak

Filter out the specific arms fetch errors before sending them to the remote logger:

log.catchErrors({
  onError(error) {
    if (!error?.message.includes('https://s-gm.mmstat.com/arms')) {
      sendErrorLog(error)
    }
  },
})

After this change, the heap stopped growing and the V8FatalErrorCallback crashes ceased.

Runtime Monitoring of V8 Heap

To continuously watch heap usage, call v8.getHeapStatistics() at regular intervals (e.g., every minute) and report the values to a monitoring platform. Example output:

{
  totalHeapSize: 26332,
  totalHeapSizeExecutable: 768,
  totalPhysicalSize: 26332,
  totalAvailableSize: 4174396,
  usedHeapSize: 19029,
  heapSizeLimit: 4194048,
  mallocedMemory: 512,
  peakMallocedMemory: 9096,
  doesZapGarbage: false
}

Detecting Leaks with node‑memwatch

The node-memwatch package emits two useful events:

stats : fires after each GC with details such as num_full_gc, num_inc_gc, and heap usage.

leak : fires when the heap grows for five consecutive GCs, providing a growth value and a reason.

When a leak event occurs, a HeapDiff can be taken to compare before/after snapshots and pinpoint leaking objects (e.g., many LeakingClass instances).

{
  "before": { "nodes": 11625, "size_bytes": 1869904 },
  "after":  { "nodes": 21435, "size_bytes": 2119136 },
  "change": {
    "size_bytes": 249232,
    "details": [
      { "what": "String", "size_bytes": -2120, "+": 3, "-": 62 },
      { "what": "Array",  "size_bytes": 66687, "+": 4, "-": 78 },
      { "what": "LeakingClass", "size_bytes": 239952, "+": 9998, "-": 0 }
    ]
  }
}

Conclusion

The article demonstrates a full workflow: start from stack analysis of V8FatalErrorCallback, explore build‑time options (disabling compilation cache, adjusting V8 flags, rebuilding Electron without pointer compression), then use Chrome DevTools Memory and Performance panels to locate the actual leak, fix the offending error‑logging code, and finally set up continuous heap monitoring with v8.getHeapStatistics and node‑memwatch. This approach can be applied to other Electron or Node.js memory‑related crashes.

Team Introduction

We are the front‑end team of Taobao Live, responsible for the live‑streaming business and exploring technologies such as interactive live rooms, game interaction, data visualization, audio‑video playback, micro‑frontends, intelligent page building, Web 3D, cross‑platform Electron development, and multi‑device architectures.

If you found this useful, feel free to follow us! 🙋‍♀️

debuggingPerformanceElectronNode.jsMemory LeakV8Chrome DevTools
Alipay Experience Technology
Written by

Alipay Experience Technology

Exploring ultimate user experience and best engineering practices

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.