Electron vs Rust Desktop Memory: Measuring Working Sets Instead of Trusting 290MB Headlines

Electron vs Rust Desktop Memory: Measuring Working Sets Instead of Trusting 290MB Headlines

pr0h0••24 min read•coding
AI Usage (83%)
On this page

The 290MB Figure Is a Claim, Not a Measurement

Discord's reported Rust desktop client is being quoted at roughly 290MB of RAM and held up as proof that Electron was the problem all along. This post does not argue that number up or down. It does something more useful: it shows how to measure a desktop client's memory properly — summed private working set across the process tree, sampled against a fixed workload on Windows, macOS, and Linux — and then uses that method to separate what a Rust plus webview rewrite genuinely changes from what it leaves exactly as it was.

What the reporting says about Discord's Rust desktop client

During the first week of November 2026 a cluster of outlets — Basic Tutorials, ShiftDelete.Net, Technobezz, BigGo Finance — published variations on one story: Discord is rebuilding its desktop client in Rust, moving off Electron, and an early Windows build reportedly uses about 290MB of RAM. The framing was identical across all of them. The rewrite gets presented as the fix for years of memory complaints, especially on Windows 11, where the Electron client has allegedly reached multi-gigabyte footprints in bad cases.

I have no reason to doubt a rewrite is happening or that a Rust build exists. What I doubt is that 290MB is a number anyone should use to reason about framework choice.

Why an idle-RAM headline is the least useful number in the story

One memory figure, from an unnamed build, at an unspecified uptime, under an unspecified workload, tells you almost nothing. Memory in a desktop chat client is a curve, not a value. It moves with how many guilds you keep open, how warm the image and emoji caches are, whether a voice channel is connected, how long the process has been alive, and which of the app's six processes you decided to add up.

Sit 290MB next to "multi-gigabyte" with no shared methodology and you don't have a comparison. You have two anecdotes in a trench coat.

What this post does instead: build a repeatable working-set comparison

So I'll skip the headline argument and hand you the rig instead: the counter to use, the workload script, the sampling window, and the table you should publish if you ever make a memory claim in public. Every command below runs against your own app — PowerShell performance counters on Windows, /proc/<pid>/smaps_rollup on Linux, footprint on macOS. A method you can execute beats a number you have to trust.

📝

I never had access to Discord's Rust build, so there is no measured 290MB-versus-X result here. Where sample output appears, it is labelled illustrative. Any numeric comparison not attributed to the reporting demonstrates the method; it is not a benchmark.

Separating Confirmed Reporting From Inference

Confirmed: the rewrite, the platform, and the rough figures the outlets repeat

These are the points the published reports agree on, dated to their own timestamps:

ClaimStatusSource of claim
Discord is rebuilding its desktop client in Rust and moving away from ElectronReported by multiple outlets, published 2026-11-05/06Aggregator summaries listed above
An early Windows build was measured at roughly 290MB of RAMReported, method not statedSame
The current Electron client has been observed at multi-gigabyte RAM in "bad cases"Reported, method not statedSame
The rewrite is framed as a response to memory complaints on Windows 11ReportedSame

The rewrite itself is the durable fact. The numbers are the part that needs work.

Unverified: exact build, workload, uptime, and what "multi-gigabyte" was measured against

Here's what the reports never say:

  • Build identity. No version string, commit, or channel. An early internal Windows build and a shipped binary are different measurement subjects.
  • Measurement window. Was 290MB taken thirty seconds after launch, or after six hours with twenty servers open?
  • Workload. Idle on the server list, or joined to voice with a screen share running?
  • Which counter. Private working set, commit charge, or the sum of every process's RSS? On the same app these differ by a factor of two or more.
  • The comparison target. Was "multi-gigabyte" measured on the same machine, same account, same uptime — or is it a remembered worst case from a support thread?

Why I will not treat an aggregator summary as a benchmark methodology

Aggregator copy inherits numbers from a primary write-up without the methodology that made them mean anything. By the fourth outlet a figure has lost its build number, its uptime, and its counter definition, and picked up a comparison it never had. I'll take the reporting as evidence that a rewrite is underway. I won't take it as evidence about Electron's cost, because that isn't what it measures.

If you want the primary-source version of this story, wait for a first-party engineering post from Discord with an actual methodology section. That's the artifact worth citing, and as of writing I haven't seen one described.

"RAM Usage" Is at Least Four Different Numbers

Private working set vs commit charge vs shared pages

On Windows, the counters people call "RAM usage" are not interchangeable:

  • Working set — pages currently resident in physical RAM for that process, including pages shared with other processes.
  • Working set – private — the resident subset mapped by this process alone. Closest thing to "what this app costs me right now".
  • Private bytes / commit charge — virtual memory the allocator has committed. Often far larger than the resident set, because it's what the OS reserves, not what's in RAM.
  • Shared pages — image pages (executables, DLLs), shared memory segments, font files, and the OS's own mapped pages. Resident once, counted in every process that maps them.

Get-Process exposes two of these and names them badly: WorkingSet64 is the total working set with shared pages included, and PrivateMemorySize64 is commit charge, not resident private memory. Neither is the number you want by itself.

How Chromium shared memory inflates the sum of per-process counters

Chromium's multi-process model means plenty of pages get mapped several times: the binary, ICU data, fonts, compositing shared memory, the V8 snapshot. Sum WorkingSet64 across eight processes and every shared page is counted up to eight times.

That's the mechanism behind most "Electron is using 2GB!" screenshots — not a lie exactly, but a sum of overlapping measurements. Linux gives you a better tool for this. PSS (proportional set size) divides each shared page by the number of processes mapping it, so summing PSS across a process tree approximates the real footprint instead of overcounting it.

Why Task Manager, Activity Monitor, and ps disagree on the same app

  • Windows Task Manager groups the process tree for known apps and shows something close to total working set including shareable pages. It also reflects memory compression and page sharing, which nothing else reports identically.
  • Activity Monitor reports "Memory" from phys_footprint, which excludes some shared and compressed pages that RSS includes.
  • ps reports RSS, which double-counts shared pages and ignores compressed memory entirely.

Three tools, three definitions, one app. Mix the tools in a comparison and you measured the tools.

The number you should pick, and the number marketing picks

For an app that ships a runtime, the defensible number is summed private working set across the process tree, sampled over time. It answers the question users actually ask: how much of my 8GB laptop does this thing eat?

Marketing picks the lowest single figure from any counter at any moment in the session, compared against the highest figure from the predecessor under a heavier workload. That's a selection effect wearing a lab coat.

Measuring a Windows Desktop Client's Working Set Properly

Pulling working-set counters with PowerShell instead of reading a screenshot

Get-Process won't give you private working set. Performance counters will. Confirm the counter names on your build first, because they aren't stable across versions:

Get-Counter -ListSet 'Process V2' |
  Select-Object -ExpandProperty Counter |
  Select-String -Pattern 'Working Set|Private Bytes'

You should see \Process V2(*)\Working Set - Private and \Process V2(*)\Private Bytes. If instance names look truncated, list them explicitly before trusting any -like filter:

(Get-Counter '\Process V2(*)\Working Set - Private').CounterSamples |
  Select-Object InstanceName, CookedValue |
  Sort-Object CookedValue -Descending |
  Select-Object -First 10

The sum function, wrapped so the rest of the script stays readable:

function Get-TreeMemory {
  param([string]$NamePattern = 'discord*')

  $ws = (Get-Counter '\Process V2(*)\Working Set - Private' -ErrorAction SilentlyContinue).CounterSamples |
        Where-Object { $_.InstanceName -like $NamePattern -and $_.Status -eq 0 }
  $commit = (Get-Counter '\Process V2(*)\Private Bytes' -ErrorAction SilentlyContinue).CounterSamples |
        Where-Object { $_.InstanceName -like $NamePattern -and $_.Status -eq 0 }

  [pscustomobject]@{
    Procs         = @($ws).Count
    PrivateWS_MB  = [math]::Round((($ws     | Measure-Object CookedValue -Sum).Sum) / 1MB, 1)
    Commit_MB     = [math]::Round((($commit | Measure-Object CookedValue -Sum).Sum) / 1MB, 1)
  }
}

Get-Counter with wildcards is slow. Hundreds of instances means seconds per call, so budget for that in the sampling loop rather than assuming a 30-second interval costs 30 seconds.

A fixed workload script: launch, idle, join a voice channel, scroll, idle again

Sampling is scriptable. Clicking isn't, so the workload has to be a written procedure you repeat exactly:

  1. Sign out, kill every process, clear the app's cache directory.
  2. Launch. Timestamp T+0. Do not touch the window.
  3. T+0:00 to T+0:05 — idle on the server list, no channel selected.
  4. T+0:05 — open one text channel with at least 50 messages of history and images. Scroll to the top once, then stop.
  5. T+0:08 — join a voice channel, stay connected, muted, no video.
  6. T+0:12 — open the member list and the emoji picker once each, then close them.
  7. T+0:15 to T+0:45 — hands off. This is where the slope lives.

Write those timestamps down. A memory number without a workload step beside it is unreadable six weeks later.

Recording samples over 30 minutes instead of one snapshot

$rows = @()
$start = Get-Date
while (((Get-Date) - $start).TotalMinutes -lt 30) {
  $m = Get-TreeMemory -NamePattern 'discord*'
  $rows += [pscustomobject]@{
    Minute       = [math]::Round(((Get-Date) - $start).TotalMinutes, 2)
    Procs        = $m.Procs
    PrivateWS_MB = $m.PrivateWS_MB
    Commit_MB    = $m.Commit_MB
  }
  Start-Sleep -Seconds 30
}

Then compute the slope instead of eyeballing it. Slope is what separates a large baseline from an actual leak:

$n  = $rows.Count
$mx = ($rows.Minute        | Measure-Object -Average).Average
$my = ($rows.PrivateWS_MB  | Measure-Object -Average).Average
$num = 0.0; $den = 0.0
for ($i = 0; $i -lt $n; $i++) {
  $num += ($rows[$i].Minute - $mx) * ($rows[$i].PrivateWS_MB - $my)
  $den += [math]::Pow($rows[$i].Minute - $mx, 2)
}
'{0:N2} MB/min' -f ($num / $den)

Sample output shape — values illustrative, not measured against Discord:

Minute  Procs  PrivateWS_MB  Commit_MB
0.02        6         284.5      712.3
0.52        6         285.9      713.0
5.02        7         301.4      748.6
12.02       8         342.8      801.2
29.52       8         351.0      806.9
slope: +2.19 MB/min

Read that shape carefully. Process count went 6 → 7 → 8 as the workload opened a channel and a voice connection. Most of the growth is step changes at workload events, not a steady climb. The pattern that indicates a leak is a flat process count with a positive slope on a pure idle run.

The results table I would publish alongside any claim

Anyone publishing a memory comparison should ship a table with these fields filled in. Less than that is a headline.

FieldWhy it mattersExample (illustrative, not measured)
Build / commitA number without an identity can't be reproduced1.0.0-canary.42 (a1b2c3d)
Machine, RAM, OS build8GB vs 32GB changes cache pressure16GB, Win 11 26H1
Uptime at each sampleSeparates baseline from slope0s / 5min / 30min
Private working set (sum)The cost on a real machine351.0 MB
Commit charge (sum)What the OS reserves806.9 MB
Peak working setCatches the first-paint spike640 MB
Process count over timeReveals renderer accumulation6 → 8
Idle CPU (5-min avg)Battery and thermals0.4%
Leak slope over 30 min idleLeak vs baseline+2.19 MB/min
Workload scriptThe procedure that produced itSteps 1–7 above

The Same Working-Set Measurement on macOS and Linux

ps and RSS limits, and when to reach for smaps_rollup

On Linux, RSS is a trap for the same reason WorkingSet64 is: shared pages get counted per process. Use /proc/<pid>/smaps_rollup, which hands you PSS pre-aggregated:

for pid in $(pgrep -f discord); do
  awk -v p="$pid" '
    /^Pss:/           {pss += $2}
    /^Private_Dirty:/ {pd  += $2}
    END {printf "%s  pss=%d kB  private_dirty=%d kB\n", p, pss, pd}
  ' /proc/$pid/smaps_rollup
done

Sum the PSS column across PIDs and you have something roughly comparable to summed private working set on Windows. Use smaps_rollup rather than smaps: the full file runs to megabytes per process and the aggregation is already done for you.

On macOS, RSS from ps is the number everyone quotes and the one that overcounts:

ps -Ao pid=,rss=,comm= | awk 'tolower($3) ~ /discord/ {rss += $2} END {printf "%.1f MB total RSS\n", rss/1024}'

For something closer to Windows' private working set, footprint reports phys_footprint, which excludes shared file-backed pages and accounts for compressed memory:

footprint -p "$(pgrep -f Discord | head -1)" | head -40

Keeping the workload identical across operating systems

The same seven steps have to run on every OS, including the "don't touch it" windows. Harder than it sounds. macOS throttles background timers more aggressively when the window is occluded, and Linux compositors disagree about whether an offscreen window keeps rendering. If you want cross-OS numbers, keep the window visible and focused on all three machines and say so in the write-up.

Why cross-platform numbers from different machines are not comparable

A 290MB macOS figure and a 290MB Windows figure are not the same measurement. Different counters, different shared-memory accounting, different RAM totals and therefore different cache pressure, different code paths for font rendering, GPU compositing, and audio. Putting them side by side in one table implies a precision nobody has.

If you must compare platforms, compare within a platform across versions of the same app. That's a real experiment. Cross-platform memory tables are entertainment.

Where Electron's Memory Actually Goes

The process model: browser, GPU, utility, and one renderer per window

Electron inherits Chromium's process architecture: a browser (main) process running your Node code, a GPU process, one or more utility processes for audio, network, and storage, and a renderer per BrowserWindow or BrowserView. The published process model docs are the map.

The practical consequence is that your app's memory is a sum over processes you didn't write, and a BrowserWindow you "closed" with hide() is a full renderer still holding its heap and its caches.

V8 heap growth versus retained DOM and image caches

When people actually profile this, the V8 heap usually isn't the problem. A chat client's renderer heap is often a few tens of megabytes. What dominates:

  • The retained DOM for a long message list — every node, every wrapper object.
  • Decoded image bitmaps held by the compositor and the image cache. A 1280×720 image is ~3.7MB decoded at 4 bytes per pixel, no matter that the JPEG on disk is 200KB. A hundred of those in scrollback is 370MB.
  • Blob URLs the app created and never revoked, pinning the backing store.

Which is how "V8 heap looks fine" and "the app is using 900MB" end up true at the same time.

What an always-on client accumulates: timers, websockets, animation frames

Discord-style clients stay open for days. They accumulate:

  • Reconnect timers. Every failed gateway reconnect can leave a setTimeout that cleanup missed.
  • Websocket buffers. A socket that reconnects without releasing its old receive buffer leaks slowly and steadily.
  • Animation frames. A requestAnimationFrame loop on a hidden window that never paused is pure waste; backgroundThrottling exists for exactly this.
  • Event listeners on window registered by a component that unmounted without removing them.

None of these are framework bugs. All of them show up as a positive slope on the 30-minute idle run, and all of them survive a rewrite to a different shell.

Idle footprint versus leak slope over hours of uptime

Two different problems, two different fixes:

SymptomWhat it meansFix
High constant footprint from T+0Runtime baseline + warm cachesProcess model, lazy windows, cache caps
Flat at 30 min, climbing at 6 hGenuine leakHeap snapshots, listener audit, timer audit
Step jumps at workload eventsExpected cache growthBudget the growth; cap the cache
Step jumps with no workloadSpawned renderer never reclaimedLifecycle audit

A rewrite changes the first row. It does nothing for rows two through four.

What a Rust Plus Webview Desktop Client Actually Changes

Shared system webview runtime versus a bundled Chromium

An Electron app ships its own Chromium and Node. Two Electron apps on the same machine with different Electron versions share essentially nothing at the code-page level, because they're different images. Five Electron apps can mean five copies of the renderer payload on disk and in RAM.

A client built on WebView2 on Windows, WKWebView on macOS, and WebKitGTK on Linux uses the OS-provided runtime. The browser process for that runtime is shared infrastructure, and the platform patches it independently. That's the real architectural win: not "Rust is faster", but "you stopped shipping a browser".

Native shell with a webview surface versus a fully native UI

"Rust client" is carrying a lot of weight in those headlines. At least three architectures hide behind it:

  1. Rust shell + embedded webview rendering the existing UI. Smallest delta from the current codebase, smallest RAM win over Electron's browser and Node processes, keeps every DOM cost.
  2. Rust shell + native UI for the chrome, webview for rich content. Real win on whatever you move native, but now you maintain two rendering models.
  3. Fully native UI. Biggest win, biggest cost.

The reports describe a Rust rebuild without specifying which of these it is. Based on how these migrations usually go, my guess (untested) is that the first shipped build lands closest to option 1 or 2, with native surfaces arriving incrementally. If a first-party description of the architecture exists, that's the document worth reading.

Startup time and binary size are separate axes from RAM

These three get conflated constantly:

  • Binary/installer size — an Electron installer carries the Chromium payload plus Node; a webview-based app carries your assets. Largest and least arguable difference here, roughly an order of magnitude in the usual case, and also the least interesting to users, who install once.
  • Cold start time — usually better without a bundled runtime spawn, but DOM construction and first render often dominate, and the shell doesn't change those.
  • Steady-state RAM — the most cited and the least affected by the shell swap.

Winning on size and startup while barely moving steady-state memory is a common and completely reasonable outcome. It just isn't the story the headlines told.

What the architecture does not fix: DOM size, image caches, renderer memory

Render the same React tree into a WebView2 surface and you get the same retained DOM, the same decoded image cache, the same per-renderer heap. WebView2 will not garbage-collect your virtualized list for you. All that changed is who pays for the browser binary and the Node main process — plus, maybe, one fewer process if the shell replaces the Node main.

Anyone telling you the framework swap alone explains a 10x memory difference is describing a difference in workload, not architecture.

The Bill Comes Due Elsewhere: Accessibility and Platform Parity

Screen reader behaviour through native-webview bridges

Electron gets accessibility nearly free: Chromium implements UIA on Windows, AX on macOS, and AT-SPI on Linux, and those implementations get exercised by the largest browser userbase on earth. A custom shell has to expose the webview's tree through the host's accessibility API, and quality varies by platform and host version.

This is the most under-discussed cost of leaving Chromium-everywhere, and the one most likely to hand you a bug report you can't close in a sprint. If NVDA or JAWS users are among your users, accessibility belongs in the migration's acceptance criteria, not in a follow-up ticket.

Keyboards, IME, high-DPI, and per-OS divergence

Chromium normalizes a pile of things you never think about: IME composition, keyboard event ordering with modifiers, high-DPI scaling and fractional scale factors, clipboard MIME negotiation, drag-and-drop semantics, font fallback. Three webviews means three sets of quirks, and the QA matrix triples.

I'm not claiming the quirks are unmanageable. I'm claiming the decision framework has to price them, because they land at the worst possible time — right when launch is scheduled.

Developer velocity: one Chromium target versus several OS webviews

Electron's actual product is a single rendering target whose Chromium version you control. One set of CSS workarounds, one set of feature detections, one set of bug reproductions, and no dependency on which WebView2 runtime the user happens to have installed.

With system webviews you inherit the platform's version skew: an evergreen WebView2 runtime that isn't, an older Safari on a machine that won't update, a distro WebKitGTK two years behind. You gain RAM and lose the ability to say "this is what the renderer does."

My position: for a team of five shipping a cross-platform client, that velocity cost usually outweighs the RAM win.

Why "Electron Is Bloated" Is the Wrong Lesson

Process boundaries, lazy loading, and budget enforcement as the real levers

The lesson I'd take from the reporting isn't "Electron bad". It's that a client which spawns renderers generously, keeps every cache warm, and never releases anything will grow no matter what renders the pixels. The levers that actually move the number:

  1. Don't create windows you aren't showing. hide() is not free; destroy() is.
  2. Throttle what's hidden. Background windows should not be running animation frames.
  3. Cap caches explicitly. Image caches, code caches, and blob URL lifetimes should have a ceiling you chose.
  4. Measure on a schedule, not once during a review.

Concrete Electron practices: defer windows, cap caches, kill idle renderers

// main.js — trimmed for brevity, error handling omitted
const { app, BrowserWindow, session } = require('electron');

app.whenReady().then(() => {
  // A disk cache ceiling you chose, not one Chromium chose for you
  app.commandLine.appendSwitch('disk-cache-size', String(64 * 1024 * 1024));

  const win = new BrowserWindow({
    show: false,
    webPreferences: {
      backgroundThrottling: true, // no rAF churn when hidden
      spellcheck: false,          // drops a dictionary + a renderer subsystem
      contextIsolation: true,
    },
  });

  win.once('ready-to-show', () => win.show());
});

// Kill renderers for windows the user actually closed
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

// Release blob URLs and caches when a heavy view goes away
function disposeHeavyView(contents) {
  contents.session.clearCache();      // disk cache
  contents.executeJavaScript(`
    for (const url of window.__blobUrls ?? []) URL.revokeObjectURL(url);
  `);
}

Two notes on the flags. disk-cache-size is read when a session is configured, so set it as early as you can — before the first window loads a URL, not after. And spellcheck: false is only free if you don't need it — for a chat client you probably do, so measure before you cut it.

Enforcing a memory budget in CI instead of discovering it in reviews

Electron exposes its own per-process metrics, and this is the piece most teams never wire up. app.getAppMetrics() returns a memory object per process, including workingSetSize in kilobytes:

// scripts/memory-budget.js — run in the main process after your test workload
const { app } = require('electron');

async function enforceBudget(budgetMB = 420) {
  const metrics = await app.getAppMetrics();
  const totalMB = metrics.reduce((sum, m) => sum + m.memory.workingSetSize, 0) / 1024;

  if (totalMB > budgetMB) {
    console.error(`memory budget exceeded: ${totalMB.toFixed(1)} MB > ${budgetMB} MB`);
    for (const m of metrics) {
      console.error(`  ${m.type}#${m.pid}  ${(m.memory.workingSetSize / 1024).toFixed(1)} MB`);
    }
    app.exit(1);
  }
  console.log(`memory budget ok: ${totalMB.toFixed(1)} MB`);
}

Worth knowing: workingSetSize is total working set, so it overcounts shared pages. That makes it an upper bound, and an upper bound is exactly what you want in a CI gate. Set the budget from your measured p95 plus 15% and let it fail the build. A budget nobody enforces is a comment.

A Fair Experiment: Comparing Two Clients Without Fooling Yourself

Controlled variables: same machine, same account, same server, same uptime

If you're going to reproduce a comparison, control these or don't publish:

  • Same physical machine, same RAM total, same OS build.
  • Same account, same guild list, same number of visible channels.
  • Same cache state at launch — a warm cache is not a neutral starting condition.
  • Same uptime points: 30 seconds, 5 minutes, 30 minutes. Not "after using it for a while".
  • Same window visibility and focus state.
  • Same measurement tool and counter on both sides.

Metrics worth capturing: private working set, total commit, startup, CPU idle

Four numbers, sampled on a schedule:

MetricHow to get itWhy
Summed private working set\Process V2(*)\Working Set - Private or PSS on LinuxPhysical RAM cost
Summed commit charge\Process V2(*)\Private BytesReservation pressure
Cold start to first paintTimestamp from process spawn to ready-to-showPerceived speed
Idle CPU over 5 minGet-Counter '\Process V2(*)\% Processor Time'Battery, thermals

Add peak working set too, since the first-paint spike is what triggers OOM on an 8GB machine.

Reading the results: what a 2x difference does and does not prove

A 2x difference in summed private working set — same machine, same uptime, same workload — proves the two builds have different baselines. That's a real result and worth publishing.

It does not prove:

  • That one app scales better with 100 guilds loaded.
  • That one app has a flatter slope over 24 hours.
  • That one app uses less memory on a different OS.
  • Anything at all about a build you didn't measure.

If the difference vanishes once you control for process count, you didn't measure a framework. You measured how many renderers each app left alive.

A Decision Framework for Leaving Chromium-Everywhere

When a native-shell rewrite is justified

  • Installer size is a product constraint (enterprise deployment, kiosks, low-bandwidth distribution).
  • Cold start is a product constraint and profiling shows runtime spawn dominates.
  • You have genuine platform integration needs that Electron's IPC surface makes expensive.
  • You have the headcount to maintain three webview targets and an accessibility test matrix.
  • Your long-uptime problems are architectural — you already know where the memory goes and the fix requires replacing the shell.

When disciplined Electron is the cheaper answer

  • Your memory slope comes from leaked listeners and timers, which the rewrite will re-implement badly.
  • Your team is JS/TS-heavy and a Rust shell would create a second hiring profile for one client.
  • Accessibility is a hard requirement and you don't have budget to build and test native-webview bridges.
  • Your users sit on managed Windows fleets with unpredictable WebView2 runtime versions.
  • The measured gap between your Electron baseline and a realistic webview baseline is under ~100MB of private working set.

The signals that you actually have a leak, not a framework problem

Test these before you open the rewrite epic:

  1. Process count grows over a 30-minute idle run with no user input.
  2. Private working set slope is positive while CPU idle sits near zero — memory growing with nothing happening.
  3. Killing a hidden window's renderer drops the total by a large step, meaning it held far more than a hidden window should.
  4. Heap snapshots show detached DOM nodes retained by your own closures, not by the renderer.
  5. Summed commit charge grows while summed private working set stays flat — you're reserving, not retaining.

Any two of those and the framework isn't your problem.

Conclusion: Measure the Working Set, Then Argue About Frameworks

The position this post takes

The reported 290MB figure is not evidence that Electron costs multiple gigabytes, because the two numbers were never measured the same way. A Rust rewrite with a system webview genuinely buys something real — no bundled Chromium, no Node main process, smaller installers, faster cold start — and that's a legitimate reason to do it. It does not buy a smaller DOM, a smaller decoded image cache, or a renderer that releases what it allocates.

If the rewrite is being framed internally as "we will fix memory", it will disappoint, because the memory problems in an always-on chat client live in the renderer's retained state, not in the shell. Move the shell if the shell is the constraint. Don't move the shell to avoid reading a heap snapshot.

The one measurement readers should run on your own app this week

Run this against your app: one 30-minute idle session, summed private working set sampled every 30 seconds, slope computed. If the slope is near zero and the baseline is high, your problem is your process model and your caches. If the slope is positive while the CPU is idle, your problem is your code, and no amount of Rust will fix it.

Everything else is a headline.

Further Reading

Share this post

More posts

Loading comments…