Skip to content

perf(cef_host): drop Chromium's spare renderer by default (~23 MB per profile) - #29

Merged
wenkaifan0720 merged 1 commit into
mainfrom
feat/renderer-memory-switches
Jul 28, 2026
Merged

perf(cef_host): drop Chromium's spare renderer by default (~23 MB per profile)#29
wenkaifan0720 merged 1 commit into
mainfrom
feat/renderer-memory-switches

Conversation

@wenkaifan0720

Copy link
Copy Markdown
Collaborator

What

Disables Chromium's spare renderer by default in cef_host, recovering ~23 MB per CEF profile.

Chromium keeps one pre-warmed spare renderer per profile so the next navigation can skip process startup. It is why renderer count measures N+1 at every N — including N=1, where a single-tile board pays for a second renderer it never uses.

Measured

Always-live bench (N mounted CefWebViews, no canvas, no lazy-spawn gate), phys_footprint via top -stats mem, shared profile, N ∈ {1,2,4,8,16}:

control spare disabled
renderers @ N=1,2,4,8,16 2, 3, 5, 9, 17 1, 2, 4, 8, 16
fixed term 149.5 MB 126.2 MB
marginal 33.02 MB/view 34.22 MB/view
0.9999 0.9998

The saving is per profile, so it compounds with however many a host app declares.

Cost: one cold renderer launch for the next browser created — first paint 38 → 69 ms (~+31 ms), 7/7 valid rounds. Nothing already rendering is touched, and no browser shares a process with another, so there is no neighbour-jank trade.

Opt back in with FLUTTER_CEF_SPARE_RENDERER=1. "0" / "" read as off, so a wrapper can neutralize an inherited var without unsetting it.

Also here

  • --disable-features is now accumulated into one comma-joined value. Chromium's CommandLine replaces on a repeated switch rather than unioning, so a second one would have silently re-enabled the MachPortRendezvous* bypass on ad-hoc builds — which blanks every tile.
  • An unconditional startup log naming the effective configuration. Two sweeps in this investigation were invalidated by silently measuring the wrong binary; one stderr write per launch is the cheapest cure.
  • A recorded negative result: --renderer-process-limit does not work here. Chromium treats it as a soft limit and still gives every CEF windowless browser a dedicated renderer — at N=16 under a cap of 8 there were 16 renderers. Its only enforced effect is declining to warm the spare. Documented in a comment so the next person doesn't spend the day we spent.

The feature name was verified against this build's vendored binary rather than from docs: SpareRendererForSitePerProcess appears verbatim in the shipped framework beside spare_render_process_host_manager_impl.cc, the source that gates on it.

Verified

$ cef_host                                  # default
[cef_host] mem-levers spare-renderer=disabled disable-features=SpareRendererForSitePerProcess,MachPortRendezvous…
$ FLUTTER_CEF_SPARE_RENDERER=1 cef_host     # opt back in
[cef_host] mem-levers spare-renderer=kept   disable-features=MachPortRendezvous…
$ FLUTTER_CEF_SPARE_RENDERER=0 cef_host     # "0" reads as off
[cef_host] mem-levers spare-renderer=disabled …

Built from source (build_cef_host.sh) against the patched campus-webauthn-h264 framework.

🤖 Generated with Claude Code

… profile)

Chromium keeps one pre-warmed spare renderer per profile so the next
navigation can skip process startup. It is why renderer count measures
N+1 at EVERY N — including N=1, where a board with a single tile pays for
a second renderer it never uses.

Measured on an always-live bench (N mounted CefWebViews, no canvas, no
lazy-spawn gate), phys_footprint via `top -stats mem`, shared profile,
N in {1,2,4,8,16}:

    renderers  N+1 -> exactly N at every N
    fixed term 149.5 -> 126.2 MB      (~23 MB back, per CEF PROFILE)
    marginal   33.02 -> 34.22 MB/view (unchanged within noise)
    r²         0.9999

The saving is per PROFILE, so it compounds with however many a host app
declares. Cost is one cold renderer launch for the NEXT browser created:
first paint 38 -> 69 ms (~+31 ms), 7/7 valid rounds. Nothing already
rendering is touched and no browser shares a process with another, so
there is no neighbour-jank trade.

Opt back in with FLUTTER_CEF_SPARE_RENDERER=1 where first-paint latency of
a newly created browser matters more than the memory. "0" and "" read as
off, so a wrapper can neutralize an inherited var without unsetting it.

Also here:

- `--disable-features` is now accumulated into one comma-joined value.
  Chromium's CommandLine REPLACES on a repeated switch rather than
  unioning, so adding a second one would have silently re-enabled the
  MachPortRendezvous* bypass on ad-hoc builds — which blanks every tile.
- An unconditional one-line startup log naming the effective configuration,
  so a measurement run can prove which binary it actually exercised. Two
  sweeps in this investigation were invalidated by silently measuring the
  wrong configuration; one stderr write per launch is the cheapest cure.
- A comment recording a NEGATIVE result so the next person doesn't repeat
  it: `--renderer-process-limit` does not work here. Chromium treats it as
  a soft limit and still gives every CEF windowless browser a dedicated
  renderer — at N=16 under a cap of 8 there were 16 renderers. Its only
  enforced effect is declining to warm the spare, a strictly weaker version
  of this change.

The feature name was verified against this build's vendored binary rather
than from docs: "SpareRendererForSitePerProcess" appears verbatim in the
shipped framework beside the source path that gates on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wenkaifan0720
wenkaifan0720 merged commit 4c45007 into main Jul 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant