WebView2 Memory Looks High? Compare Task Manager with a V8 Heap Snapshot
Desktop apps built with WebView2, Tauri, or similar webview stacks can look memory-hungry in Task Manager. That does not automatically mean the JavaScript heap is leaking.
V8 can retain OS pages after objects are collected. Task Manager shows process memory. It does not tell you whether live JavaScript objects are still growing.
Before rewriting your state layer, compare Task Manager with a real heap snapshot.
The false-positive leak
A common observation:
Task Manager:
app process grows from 120 MB to 260 MB
App behavior:
no obvious slowdown
no growing list in UI
clearing data does not immediately lower RSS
This can be normal V8 memory behavior. V8 may keep memory pages reserved for reuse instead of returning them to the OS immediately.
The number you need is live JavaScript heap, not only process RSS.
Take a heap snapshot
Use Edge or Chrome DevTools attached to the WebView2 instance:
- Open DevTools for the webview.
- Go to the Memory panel.
- Take a heap snapshot.
- Repeat after the suspected leak scenario.
- Compare retained objects and heap size.
If Task Manager is high but the heap snapshot is small and stable, you probably do not have a JS object leak. You may have normal V8 page retention.
If both Task Manager and heap snapshots grow monotonically, then you have a stronger leak signal.
High-frequency state updates create GC pressure
Even without a permanent leak, high-frequency updates can make memory and CPU look bad.
This is common in desktop tools:
- serial logs
- terminal output
- file watcher streams
- telemetry graphs
- build logs
- trace viewers
The inefficient pattern is updating UI state for every event:
socket.onmessage = (event) => {
useLogStore.setState((state) => ({
lines: [...state.lines, event.data],
}));
};
At dozens of events per second, that creates many arrays and short-lived objects.
Buffer events and flush at UI speed
Batch events before updating React or Zustand:
const buffer: string[] = [];
socket.onmessage = (event) => {
buffer.push(event.data);
};
setInterval(() => {
if (buffer.length === 0) return;
const batch = buffer.splice(0, buffer.length);
useLogStore.setState((state) => ({
lines: [...state.lines, ...batch].slice(-10_000),
}));
}, 200);
This reduces state writes from "one per event" to "five per second." For logs and monitoring views, a 200 ms flush interval is usually responsive enough and much easier on the garbage collector.
Remove dead store fields
Zustand, Redux, and other state stores can carry fields that no component reads anymore. If those fields are copied on every update, they still allocate memory.
Audit top-level fields:
rg "unusedFieldName" src
If no component reads a field and no persistence format requires it, remove it. Dead arrays are especially costly in high-frequency update paths.
Separate leak diagnosis from optimization
Use this decision rule:
RSS high, heap stable
-> not enough evidence for a JS leak
-> optimize update frequency if performance suffers
RSS high, heap grows, retained objects grow
-> likely JS retention leak
-> inspect retaining paths
CPU high, heap stable
-> likely update/render pressure
-> batch events and reduce renders
Do not label a WebView2 app "leaking" based on Task Manager alone.
References
- Microsoft Edge DevTools: Record heap snapshots
- Chrome DevTools: Record heap snapshots
- V8 blog: speeding up heap snapshots
Summary
WebView2 process memory and V8 live heap are not the same thing. Use heap snapshots to confirm retention, then optimize high-frequency state updates with buffering. That separates real leaks from normal V8 page retention and avoidable GC pressure.