← ./articles

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:

  1. Open DevTools for the webview.
  2. Go to the Memory panel.
  3. Take a heap snapshot.
  4. Repeat after the suspected leak scenario.
  5. 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

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.