Profiling Memory in Android WebViews

Your hybrid app’s embedded web screens slow down or go blank after a while, crash reports mention the renderer process being killed, and you cannot see the web content’s memory from Android Studio’s profiler. This guide from Remote Debugging Memory on Mobile Browsers, in the Browser DevTools & Performance Profiling Workflows section, shows how to attach Chrome DevTools to a WebView, where WebView memory actually lives on the device, and how to survive renderer terminations.

Symptom Root Cause Immediate Action Measurable Impact
WebView not listed in chrome://inspect Web contents debugging disabled in the app Call WebView.setWebContentsDebuggingEnabled(true) in debug builds Full DevTools, including Memory panel, on device
Android Studio profiler shows low app memory, yet screens go blank WebView content runs in a separate renderer process Measure the renderer with dumpsys meminfo Reveals the process that actually grows
White screen after returning to the app Renderer killed under memory pressure Implement onRenderProcessGone and recreate the WebView App recovers instead of showing a blank screen
Memory grows each time a web screen opens Old WebView instances not destroyed Call destroy() on removed WebViews; avoid holding Activity context Renderer memory returns after screen closes
JS heap fine, footprint high Images, canvases and DOM outside the JS heap Compare snapshot totals with process PSS Targets the right kind of memory

Root Cause: The WebView Renderer Is Its Own Process

Android System WebView is built from the Chromium codebase. Since Android 8.0 it runs web content out of process by default: the app process hosts the WebView view and the browser-side logic, while JavaScript, the DOM, layout and decoded images live in a separate, sandboxed renderer process. That split has two consequences for memory work.

First, the app-side tools do not see web memory. Android Studio’s memory profiler attaches to your app’s process and reports its Java/Kotlin and native heaps. The megabytes consumed by a leaking single-page app inside the WebView are in the renderer process, which appears in adb shell dumpsys meminfo as a separate sandboxed_process entry associated with WebView. Teams frequently conclude “memory is fine” from the app profiler while the renderer grows unchecked.

Second, the renderer can be killed independently. When the device runs low on memory, Android’s low-memory killer may terminate the renderer process — especially while the app is in the background. The WebView then has no content; without handling, users see a white screen when they return. Since API level 26 the app is told via WebViewClient.onRenderProcessGone(), which reports whether the renderer crashed or was killed, and must destroy and recreate the affected WebView.

For the JavaScript side, a WebView behaves like any Chromium page once you enable debugging: chrome://inspect on a desktop Chrome, with the device connected over USB as in debugging Android Chrome memory over USB, lists each debuggable WebView and opens full DevTools, including heap snapshots and allocation profiles. The standard leak workflow — the three-snapshot technique — then applies unchanged.

Where WebView memory lives The app process holds activities, the WebView view object and Java and native heaps, measured by the Android Studio profiler. A separate sandboxed renderer process holds the V8 JavaScript heap, DOM and layout, and decoded images, measured by Chrome DevTools via chrome://inspect and by dumpsys meminfo. The low-memory killer can terminate the renderer independently, triggering onRenderProcessGone in the app. App process Activities, WebView view object Java/Kotlin + native heaps seen by Android Studio profiler Renderer (sandboxed_process) V8 JS heap, DOM, layout decoded images, canvases seen by chrome://inspect + dumpsys IPC low-memory killer may end this process onRenderProcessGone() → recreate WebView

Step-by-Step Fix

  1. Enable WebView debugging in debug builds. Add WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG) in your Application.onCreate. Verification: with the device connected by USB and USB debugging on, chrome://inspect/#devices on your desktop lists the app’s WebView pages.
  2. Record the renderer’s memory from the device. Run adb shell dumpsys meminfo and find the WebView renderer process (listed as sandboxed_process with your app’s WebView), then run adb shell dumpsys meminfo <pid> for its breakdown. Verification: you have the renderer’s total PSS in MB before exercising the web screens.
  3. Exercise the suspected flow ten times. Open and close the web screen, or navigate within the SPA, ten times, then read the renderer PSS again. Verification: growth per repetition is visible (for example +8 MB per open).
  4. Snapshot the JS heap through DevTools. Click inspect for the WebView, open Memory, and apply the three-snapshot technique around the same flow. Verification: you can tell whether the growth is in the JS heap (snapshot totals grow) or elsewhere (PSS grows, snapshots flat).
  5. Check WebView lifecycle on the app side. Ensure closed screens call webView.destroy() after removing it from the view hierarchy, and that no static field or long-lived object keeps a WebView or its Activity context. Verification: after closing the screen, the renderer’s PSS falls and the Android Studio profiler shows no retained WebView instances.
  6. Handle renderer termination. Override onRenderProcessGone, remove and destroy the dead WebView, recreate it, and restore state. Verification: killing the renderer with adb shell kill <renderer pid> on a debug build no longer leaves a blank screen.
Renderer PSS across ten screen opens Before the fix, the renderer's proportional set size grows from 120 to 205 megabytes across ten opens of the web screen. After destroying WebViews on close and removing a JS listener leak, it rises to 140 megabytes on the first open and stays flat. 220 MB 100 MB web screen opens 1 → 10 before: +8.5 MB per open after: flat at ~140 MB

Command and Code Reference

Use case: enable debugging and recover from renderer termination (Kotlin).

// Application.kt — debuggable WebViews in debug builds only
class App : Application() {
    override fun onCreate() {
        super.onCreate()
        WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
    }
}

// WebScreenFragment.kt — recover when the renderer is killed for memory
webView.webViewClient = object : WebViewClient() {
    override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean {
        // detail.didCrash() == false means the system killed it (usually memory)
        container.removeView(view)
        view.destroy()                       // release the dead instance
        webView = createWebView().also { container.addView(it) }
        webView.loadUrl(lastUrl)             // restore state from your own storage
        return true                          // we handled it; do not crash the app
    }
}

override fun onDestroyView() {
    container.removeView(webView)
    webView.destroy()                        // frees the renderer-side page
    super.onDestroyView()
}

Use case: sample renderer memory from a shell while testing.

# List processes and find the WebView renderer for your package
adb shell dumpsys meminfo | grep -i -E "sandboxed_process|webview"

# Detailed breakdown for one PID (TOTAL PSS is the number to track)
adb shell dumpsys meminfo 12345 | grep -E "TOTAL|Native Heap|Graphics"

# Sample every 10 s while you run the flow; stop with Ctrl+C
while true; do adb shell dumpsys meminfo 12345 | grep "TOTAL PSS"; sleep 10; done

Verification and Regression Prevention

A fix is complete when the renderer’s PSS returns to a stable plateau after repeated opens, heap snapshots taken through chrome://inspect show no growing constructors, and the Android Studio profiler shows no retained WebView or Activity instances after closing web screens. Test on a low-memory device or emulator image as well; renderer kills that never happen on a flagship phone are routine on 2–3 GB devices.

For regression protection, script the open/close flow with an instrumentation test and log dumpsys meminfo for the renderer before and after ten iterations, failing when growth exceeds a set budget. On the web side, the same automated leak tests you run in desktop Chrome catch most JavaScript leaks before they reach the WebView. Budgets for memory-constrained devices are discussed in setting memory budgets for low-end devices.

Regression check for a WebView screen Script the open and close flow with an instrumentation test, log dumpsys meminfo for the renderer process, take a heap snapshot through chrome://inspect, and confirm the Android Studio profiler shows no retained WebView or Activity after closing web screens. Repeat on a 2 to 3 GB device image. Scripted open/close instrumentation test dumpsys meminfo renderer PSS plateaus chrome://inspect no growing constructors Profiler no retained WebView/Activity repeat on a 2–3 GB device image, where renderer kills are routine

Edge Cases and Gotchas

Several WebViews share one renderer

All WebViews in an app typically share a single renderer process. One leaking web screen therefore affects every other embedded page, and a renderer kill takes all of them down at once — onRenderProcessGone is called for each affected WebView, and each must be recreated.

Release builds are not debuggable

chrome://inspect only lists WebViews in apps that enabled content debugging. Reproduce production issues with a debug or internal build that shares the same web bundle, or add a hidden developer setting that enables debugging for internal testers.

JavaScript interfaces retain Java objects

Objects exposed with addJavascriptInterface are referenced from the renderer side for the lifetime of the page. Exposing an Activity or a large manager object keeps it alive until the WebView is destroyed. Expose small, dedicated bridge objects and destroy the WebView when its screen closes.

Background timers keep working

A WebView in a background activity may keep running JavaScript timers unless you call onPause() and pauseTimers() appropriately. Pause them when the screen is hidden to cut both CPU use and allocation that would otherwise build up while nobody is looking.

Frequently Asked Questions

Why can’t Android Studio’s profiler see my web app’s memory?

Because WebView content runs in a separate sandboxed renderer process, while the profiler attaches to your app’s process. Use dumpsys meminfo for the renderer’s totals and Chrome DevTools via chrome://inspect for its JavaScript heap.

What should happen when onRenderProcessGone is called?

Remove the affected WebView from the view hierarchy, call destroy() on it, create a new WebView, and reload content from state you saved yourself. Returning true tells the system you handled it; returning false lets the app crash, which is the default if you do nothing.

Can I use the same leak-detection tests as for Chrome?

Largely, yes. The JavaScript engine and DOM are Chromium’s, so leaks found in desktop Chrome with Puppeteer or Playwright usually reproduce in the WebView. Device-specific issues — memory limits, renderer kills, image decoding costs — still need testing on real hardware.