Memory & the process model

A Cefrium browser is a full Chromium engine, not a lightweight view. This guide explains what that costs, how to show many pages without growing memory without bound, and the Android process knobs Cefrium sets for you so a multi-page app stays stable on every device.

What a browser actually costs

Each Cefrium browser drives a real out-of-process Chromium renderer (often tens to hundreds of MB), the same multi-process model Chrome uses. A cross-origin <iframe> (a YouTube embed, an ad, a payment frame) is isolated into its OWN renderer process under site isolation. So a single page can be two or more processes.

This is the source of Cefrium's power (a true, auditable, full-Chromium engine you control) and its weight. The rule of thumb: keep only as many live browsers as you actually need on screen, and let the rest go.

Showing ONE page at a time? You do not need any of this. Reuse a single CefriumView and swap content with navigator.loadUrl(...) / loadHtmlWithBaseUrl(...). Pooling matters only when you want the last few pages to stay warm.

Many pages, bounded memory: CefriumBrowserPool

For a screen that shows many different pages over time -- an article feed, a tab strip, a gallery of pages -- keeping one browser alive per item you ever opened grows memory without bound. CefriumBrowserPool keeps at most maxAlive browsers alive and CLOSES the least-recently-used one when you open another. Memory stays bounded by maxAlive, not by how many pages you have opened in total; the most recent N keep their state (scroll, video position) for an instant return.

// Compose
val pool = rememberCefriumBrowserPool(maxAlive = 3)

// reuses the warm browser for this key, or creates one that loads `url`
val state = pool.stateFor(page.id, page.url)
CefriumView(state = state, modifier = Modifier.fillMaxSize())

// Opening a 4th distinct key closes + evicts the least-recently-used browser.
// Its renderer (and any cross-origin iframe renderers) are released.

The pool owns its states: when it leaves composition every pooled browser is closed. Do not also wrap a pooled state in rememberCefriumViewState (that would double-own it). For the View API, hold the browsers in your own bounded map and call browser.close() on eviction.

Eviction is a REAL close: Cefrium completes Chromium's window-destruction handshake on Android, so the WebContents and every one of its renderer processes (host page + cross-origin iframes) are actually destroyed, not just detached. You can watch this with the diagnostics below.

Backgrounding: pause inline media automatically

When the host is backgrounded (HOME, recents), a playing <video> should stop. CefriumView wires this to the host lifecycle for you; the default is PAUSE_ON_STOP. Pick another policy, or take full control:

CefriumView(
    state = state,
    backgroundMediaPolicy = BackgroundMediaPolicy.PAUSE_ON_STOP, // default
    // or full control:
    onHostLifecycle = { event, browser -> /* mute / pause / nothing */ },
)

Policies: PAUSE_ON_STOP (default), PAUSE_ON_PAUSE, MUTE_ON_STOP, NONE (e.g. background-audio players that should keep playing).

The Android process model (and one thing you must NOT do)

Cefrium runs Chromium's full multi-process model: renderers, GPU and utility processes run as sandboxed Android services. The SDK declares 40 sandboxed render-process slots (Chrome's own default) so a pool of pages, each with a cross-origin iframe, never exhausts the pool -- including the brief overlap while an evicted browser is still closing as the next one opens.

Do not declare child-process services in your app manifest. Older setups copied a block of <service android:name="org.chromium.content.app.SandboxedProcessService0"> with NUM_SANDBOXED_SERVICES=5 into the app's AndroidManifest.xml. That OVERRIDES the SDK and caps the pool at 5 -- on process-per-browser devices the system then kills a still-needed foreground renderer ("isolated not needed") and the app crashes. The SDK AAR provides these services (40 sandboxed, 3 privileged). Remove any such block; the merged APK manifest should read NUM_SANDBOXED_SERVICES=40 (aapt2 dump xmltree --file AndroidManifest.xml app.apk).

Cefrium also marks the visible browser's renderer with an IMPORTANT Android binding (and detached pooled browsers MODERATE), the same way Chrome protects its active tab. This keeps aggressive OEM process managers from reclaiming the renderer you are actually looking at while still letting them reclaim idle pooled ones first. It is automatic; there is nothing to configure.

Diagnostics: watch the renderers

Cefrium exposes the engine's live process/frame state so you can confirm a pool is bounded (not leaking) on your own devices. All are static; the process-counting ones must be read on the main thread (they return -1 otherwise).

Call (on CefriumBrowser)What it tells you
activeRenderProcessCount()Live renderer processes (excludes utility/GPU and the spare). Should track maxAlive, not total pages opened.
totalRenderFrameHostCount()Total live frames (host pages + iframes). Flat across many pages = bounded; climbing = a leak.
closedBrowserCount()Cumulative browsers that finished closing. Grows as a pool evicts = closes are completing.
zeroFrameRenderProcessCount()Renderers hosting zero frames (reclaim candidates).
liveBrowserCount()Browser instances not yet closed.
// Open 40 pages in a maxAlive=3 pool and confirm it is bounded:
Log.i("mem", "rp=" + CefriumBrowser.activeRenderProcessCount()
            + " frames=" + CefriumBrowser.totalRenderFrameHostCount()
            + " closed=" + CefriumBrowser.closedBrowserCount()
            + " live=" + CefriumBrowser.liveBrowserCount());
// Healthy: rp and frames stay flat (~maxAlive x frames-per-page) while
// closed grows; live == maxAlive.

No other Android embeddable gives you renderer-process visibility like this. See the API reference for the full list.