oxmc 0f83d848d1 Remove --always-compact V8 flag: no longer recognized in newer V8
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.
2026-07-26 04:21:46 -07:00

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: < 4096MB low, < 8192MB medium, 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 13 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

  • keepBackgroundActive defaults 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 set enable: { keepBackgroundActive: true } if you need background windows to stay fully responsive (e.g. continuous audio/streaming).
  • enable-features/disable-features merge instead of clobber. Chromium's commandLine.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. computeFlags merges repeated enable-features/disable-features calls into one comma-joined switch internally so nothing gets lost.
  • global.gc actually works in the main process. --js-flags=--expose-gc set via commandLine.appendSwitch only 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 working global.gc. startMemoryMonitor self-enables it via v8.setFlagsFromString('--expose-gc') + vm.runInNewContext('gc') instead. Exposed standalone as enableMainProcessGc(), and toggleable via { exposeGc: false }.
  • Deep clean is conservative by default. deepCleanStorages defaults to ['serviceworkers', 'cachestorage', 'shadercache'] — memory-heavy but not user session state — deliberately excluding cookies/localstorage/indexdb so 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.
  • rendererProcessLimit caps --renderer-process-limit by memory tier — matters most for apps that open many BrowserWindows/WebContentsViews, each normally its own process.
S
Description
Reduce Electron's memory/CPU footprint: tiered Chromium/V8 command-line flags, a runtime GC/cache monitor with total-process memory tracking, and per-window memory helpers.
Readme
64 KiB
Languages
TypeScript 100%