V8's --js-flags parser stops at the first unrecognized flag and silently drops everything after it in the same string. --always-compact was removed from V8 at some point after the versions bundled in Electron ~41; on newer Electron (confirmed broken on a castlabs v43.0.0+wvcus build) it killed --optimize-for-size right after it in every js-flags string that had it, even though --optimize-for-size itself is still valid. Verified via --v8-options against the actual bundled V8 that gc-global/optimize-for-size/ max-semi-space-size/initial-heap-size are still real flags — only always-compact needed to go.
electron-performance
Reduce Electron's memory/CPU footprint: tiered Chromium/V8 command-line flags (memory-, GPU-, and Raspberry-Pi-generation-aware), a runtime GC/cache monitor with total-process memory tracking, and per-window memory helpers.
CommonJS output, written in TypeScript (ships .d.ts).
Install
npm install electron-performance
@oxmc/node-gpuinfo is an optional native dependency used for GPU-tier detection; if it
fails to install on a given platform/arch, GPU detection just degrades to an empty list
(flags fall back to the "no GPU" branch) instead of breaking the install.
Usage
Three independent pieces. Wire in whichever you need.
1. Command-line flags — call before app.whenReady()
const { app } = require('electron');
const { applyCommandLineFlags } = require('electron-performance');
// Must run before app is ready — Chromium ignores switches set later.
const system = applyCommandLineFlags(app);
Disable specific categories, or override the computed heap/cache sizes:
applyCommandLineFlags(app, {
enable: { spellcheck: false, notifications: false }, // leave these Chromium defaults alone
jsHeapSizeMB: 1024, // override the auto-tiered --max-old-space-size
});
computeFlags(system, options) is the pure version (no app, just returns the flag
list) if you want to inspect/test the decision before applying it:
const { getSystemInfo, computeFlags } = require('electron-performance');
const system = getSystemInfo();
console.log(computeFlags(system));
2. Runtime memory monitor — call after app.whenReady()
Periodic + pressure-triggered GC, periodic session-cache clearing, and a pressure-triggered
deep clean: when total RSS across every process (main, renderers, GPU, utility — via
app.getAppMetrics(), not just main's own heap) crosses a threshold, it wipes service
worker/cache-storage data on every window's session and forces GC. That total-memory check
matters — for a multi-window or webview-style app, almost all memory lives in renderer
processes, which process.memoryUsage() can't see at all.
const { app, BrowserWindow } = require('electron');
const { startMemoryMonitor } = require('electron-performance');
app.whenReady().then(() => {
const monitor = startMemoryMonitor({ app, BrowserWindow }, { system }); // reuse the SystemInfo from step 1
// monitor.stop() to tear down
// monitor.forceGc() / monitor.deepClean() to run either immediately
// monitor.getMemoryUsage() / monitor.getTotalMemoryUsage() to inspect on demand
});
Omit BrowserWindow (with clearCache: false, deepClean: false) if you only want the GC loop.
3. Per-window helpers
const { getRecommendedWebPreferences, attachWindowMemoryOptimizations } = require('electron-performance');
const win = new BrowserWindow({
webPreferences: getRecommendedWebPreferences({ preload: '...' }),
});
attachWindowMemoryOptimizations(win, {
onSettleHidden: () => monitor.forceGc(), // clears this window's cache ~15s after hide/minimize, cancels if shown again
muteAudioOnHide: true, // opt-in: mute audio/video while hidden, unmute on show
});
Tuning tiers
- Memory tier:
< 4096MBlow,< 8192MBmedium, else high — drives--max-old-space-size,--max-semi-space-size, disk cache size, and renderer process limits. - GPU tier: discrete GPU present / integrated-only / none — drives GPU-acceleration
flags (
@oxmc/node-gpuinfo, filtered for software/virtual renderers). - Raspberry Pi: detected via
detect-rpi; generations 1–3 get aggressive tuning (GPU disabled, tiny heap), gen 4 moderate, gen 5 light — this branch takes priority over the generic memory/GPU tiering above.
All of the above can be disabled per-category via options.enable (see FlagCategories
in types.ts), or bypassed entirely by passing your own SystemInfo.
Notable design decisions
keepBackgroundActivedefaults to off (opt-in). It's tempting to disable Chromium's renderer backgrounding/timer-throttling for snappier background windows, but that undoes one of the cheapest, biggest memory/CPU wins Chromium gives you for free: letting hidden/minimized windows' renderers actually idle. Only setenable: { keepBackgroundActive: true }if you need background windows to stay fully responsive (e.g. continuous audio/streaming).enable-features/disable-featuresmerge instead of clobber. Chromium'scommandLine.appendSwitch(name, value)overwrites a switch if called more than once with the same name rather than merging comma-lists — call it from two different tuning branches and the second silently drops the first.computeFlagsmerges repeatedenable-features/disable-featurescalls into one comma-joined switch internally so nothing gets lost.global.gcactually works in the main process.--js-flags=--expose-gcset viacommandLine.appendSwitchonly reaches renderer/utility processes — Electron's main process already has V8 initialized by the time app code runs, so that switch alone never gives main a workingglobal.gc.startMemoryMonitorself-enables it viav8.setFlagsFromString('--expose-gc')+vm.runInNewContext('gc')instead. Exposed standalone asenableMainProcessGc(), and toggleable via{ exposeGc: false }.- Deep clean is conservative by default.
deepCleanStoragesdefaults to['serviceworkers', 'cachestorage', 'shadercache']— memory-heavy but not user session state — deliberately excludingcookies/localstorage/indexdbso a deep clean can't silently log a user out of a page they're viewing. Pass your own list for a more aggressive clean if that tradeoff is acceptable for your app. disableBackForwardCache(default'auto', low memory tier only) turns off BFCache, which otherwise keeps a fully-frozen copy of every recently-navigated-away-from page resident in memory for instant back/forward.rendererProcessLimitcaps--renderer-process-limitby memory tier — matters most for apps that open many BrowserWindows/WebContentsViews, each normally its own process.