Extracted and generalized from hand-tuned memory optimizations in an Electron app template: memory/GPU/Raspberry-Pi-tiered command-line flags, a runtime GC + total- process memory monitor with pressure-triggered deep clean, and per-window hide/ minimize memory helpers.
132 lines
6.0 KiB
Markdown
132 lines
6.0 KiB
Markdown
# 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
|
||
|
||
```sh
|
||
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()`
|
||
|
||
```js
|
||
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:
|
||
|
||
```js
|
||
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:
|
||
|
||
```js
|
||
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.
|
||
|
||
```js
|
||
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
|
||
|
||
```js
|
||
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 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
|
||
|
||
- **`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.
|