Skip to main content
  1. Insights/

Insights · Deep Dive

Flutter Platform-View ANRs on Mali/PowerVR GPUs

··11 mins·
Mjashem
Deep-Dive Flutter Platform-View Webview Gpu
Mohammad Jashem
Author
Mohammad Jashem
Senior full-stack mobile engineer with 7+ years building production apps end-to-end for Android, iOS, Web, and TV. Flutter, React Native, Expo — plus backend, CI/CD, and infrastructure. Architecture-first, AI-native delivery. Available for freelance and Upwork engagements.
Table of Contents

This is the investigation I wish someone had handed me before I spent six weeks patching a Flutter platform-view ANR I couldn’t fix in app code. It’s the engineering detail behind our full comparison of Flutter vs React Native vs Expo and the failure mode that ended in migrating Kahf Kids off Flutter. Dev forums are full of “Flutter WebView ANR on Redmi Note” threads with no real answers — most blame the WebView, tell you to enable hybrid composition, or say clear the cache. None of that fixes this.

I’m Mohammad Jashem, a Senior Mobile Architect. The trace below is real (redacted), the device matrix is real, and the decision it forced is documented in the Kahf Kids Flutter deep dive.

The symptom
#

The first signal was Play Console vitals. Foreground ANR rate ticking up on a narrow slice of devices, roughly eighteen months after the Kahf Kids Flutter build shipped. Not a crash — an ANR. The app froze long enough for Android’s watchdog to offer “close app.” For a parental-control product whose promise is “your child cannot get stuck or escape,” a frozen player is a broken product, not a regression.

The cluster was specific and reproducible:

  • Samsung Galaxy A12, A10s, A03s — Mali-G52 MC2 / Mali-G57 MC2, Android 10–12, driver r32p1–r36p1.
  • Xiaomi Redmi Note 10/11/8 (2021) — Mali variants on budget SKUs failed the same way.
  • Realme C25s, C31, C33 — PowerVR GE8320, Android 11–12.
  • Android TV boxes with Mali-450/470 — same failure mode, but on TV it wasn’t degraded, it was unusable. D-pad focus traversal compounds it because focus events also touch the platform view.

A typical ANR trace from a Redmi Note 11 (Mali-G52 MC2, Android 11), redacted:

"main" prio=5 tid=1 Native
  | sysTid=23417 nice=-10 cgrp=default sched=0/0
  native: #00 pc 00000000000f9c34  /system/lib64/libc.so (__ioctl+4)
          #01 pc 000000000008e4ec  /system/lib64/libc.so (ioctl+44)
          #02 pc 000000000005a134  /system/lib64/libEGL.so (eglWaitGL+88)
          #03 pc 000000000006cc2c  /system/lib64/libflutter_engine.so
          #04 pc 00000000004d1d10  /libflutter_engine.so (FlutterPlatformView::OnDisplay+64)

"FlutterRasterThread" prio=5 tid=3 Native
  | sysTid=23429
  native: #00 pc 00000000000f5b90  /system/lib64/libc.so (syscall+16)
          #01 pc 0000000000063e8c  /vendor/lib64/egl/libGLES_mali.so (MaliglesWaitFence+120)

"1.main" Blocked — waiting on <0x0dc1c7d8> (android.view.HardwareRenderer)

Read that trace carefully. The main thread is in eglWaitGL, parked inside libflutter_engine.so, on a fence the Mali driver (MaliglesWaitFence) isn’t clearing. The Raster thread is parked in the same call. The main thread then blocks on the HardwareRenderer lock the Raster thread holds. Five seconds and ActivityManagerService fires the ANR. No Java from your app on that stack. No Dart. No fix you can ship in pubspec.yaml.

The setup
#

Why platform views were in the hot path at all. Kahf Kids is a curated kids’ video app — the core flow is a child watching a video, and the product contract is that the child cannot leave that flow. No “open in YouTube,” no outbound link, no escape gesture.

That contract is enforced by a custom Flutter player layer rendered on top of a YouTube WebView embed. The WebView plays the video; the Flutter overlay captures the surface, intercepts navigation intents, and blocks the exit paths. A child pressing Back or swiping up is caught by the overlay, not the WebView.

That architecture puts a platform view in the hot path by definition: an Android WebView is an android.view.View, and to show one inside a Flutter widget tree, Flutter must compose it into its own Skia/Impeller surface. Every WebView, map, camera preview, and AR session in Flutter pays this tax — and when the overlay above the WebView is itself Flutter-rendered, both sides of the boundary are under sustained GPU load simultaneously. That’s the worst-case shape for the failure mode below.

Root cause
#

Flutter platform views, on Android, have historically shipped in two composition modes:

  • Virtual Display (legacy default, pre-v1.22). The platform view renders to an off-screen Surface, composited as a texture inside Flutter’s render tree. Pro: full GPU isolation, no surface contention. Con: touch input at native coordinates is wrong, accessibility and IME are broken, text selection is wrong. It’s a video of a View, not a View.
  • Hybrid Composition (v1.22+, new default). Flutter asks the Android framework to composite the real native View into the same Surface Flutter is rendering to. Touch, accessibility, IME, and text all work because the real View is on stage. The cost: Flutter’s Raster thread and the platform view’s render path must synchronize on a shared GPU context. This is the path that matters for ANRs.
  • Texture Layer Hybrid Composition (the newer mitigation). A middle ground — the platform view is composited via a TextureLayer rather than full HC, reducing (not removing) the synchronization surface.

In Hybrid Composition, the handshake looks like this: Flutter’s Raster thread acquires a GPU context lock to render its own frame; the Android HardwareRenderer for the platform view also needs the GPU; the two threads agree on a fence the GPU driver must signal before either proceeds. On well-behaved drivers (Adreno on a Pixel, Apple’s GPU family) the fence clears in microseconds. On certain Mali and PowerVR driver builds — specifically the older midgard/bifrost Mali userspace blobs on the budget SoCs above, and the Rogue-series PowerVR GE8320 blobs — the fence stall is unbounded under load. The Raster thread holds its lock waiting on eglWaitGL. The main thread blocks on the HardwareRenderer lock the Raster thread holds. Five seconds. ANR.

I want to be precise because “Flutter WebView ANR” is underspecified in every forum thread I read. This is not jank. Jank is dropped frames and a live UI. This is a context-lock: the GPU fence between Flutter’s Raster thread and the platform view’s render thread fails to signal, both threads stall, and the main thread transitively blocks on a lock held by one of them. The app is alive in memory; it cannot paint or process input — which is why am kill doesn’t always recover it cleanly and why users reboot the device.

Why the cheap GPUs specifically? Not because Mali and PowerVR are bad — they power hundreds of millions of functional devices. Because the driver builds the OEM shipped on these budget SKUs were old, had known fence-signaling bugs in their userspace EGL, and were never going to receive an OTA. The device is stuck on a 2019-era libGLES_mali.so for life. You cannot recompile that blob. That’s the wall.

Reproducing and debugging
#

The forum threads that go unsolved usually die at this step. “Can’t reproduce on my device” is the standing reply, because the engineer trying to help has a Pixel, not a Redmi Note 11. The first thing I did was stop trusting emulators and flagship devices entirely.

Reproduction rig:

  • Real hardware only. A Samsung A12 (Mali-G52), a Redmi Note 11 (Mali-G57 variant), a Realme C25s (PowerVR GE8320). Bought used, plus a Pixel 4a as the “well-behaved driver” control.
  • Long-running load. The ANR doesn’t fire on a 10-second smoke test. It fires under 5–20 minutes of continuous WebView video with the Flutter overlay active. Most reproductions came inside 15 minutes.
  • adb shell atrace + Perfetto. atrace -c -b 32768 gfx view sched freq idle -t 30 -o /sdcard/trace.trace while reproducing, then adb pull and open in Perfetto. The UI immediately shows the Raster thread parked on MaliglesWaitFence. Classic systrace is deprecated; Perfetto handles the 20-minute capture you need here — -b 32768 minimum so the failing window survives.
  • Android Studio GPU Inspector for the second pass, to confirm the fence stall rather than guess. On PowerVR, Imagination’s PVRTune is the only tool that reads Rogue-series counters correctly.
  • Play Console vitals for population evidence. One device on a bench is an anecdote; ANRs clustering across thousands of sessions on the same SoC/GPU/driver is evidence.

The decisive finding was negative: the Pixel 4a, on the same Flutter engine and same app build, never ANR’d across a week of bench runs. The Redmi Note 11 ANR’d inside 15 minutes. That isolates the variable to the GPU and its driver — not your Dart code, not the WebView, not the network.

Options considered
#

What I tried, in order, and what each actually did.

  1. Force Virtual Display mode via the legacy composition flags. ANRs dropped sharply because the surface-contention path was removed. Cost: touch input broke in edge cases, IME misbehaved, accessibility was wrong. Not acceptable for a parental-control overlay. Bandage, not a fix.
  2. Texture Layer Hybrid Composition — the engine-level mitigation Flutter shipped for this class of bug. Reduced ANR frequency on Mali by ~60–70% in our vitals. Did not eliminate it; on PowerVR the reduction was weaker. Still the best no-cost mitigation, but you can’t ship a product on “ANRs 70% less often.”
  3. Disabling Impeller and forcing Skia on the affected devices. No measurable effect; the fence stall is below the Impeller/Skia split.
  4. Throttling the overlay’s repaint — making the Flutter layer above the WebView near-static. Helped on jank, marginal on the ANR because the lock can fire on a single frame boundary.
  5. A native Kotlin player behind a MethodChannel/PlatformView, removing the WebView entirely. Works — the ANR goes away — but you’ve rewritten your core flow in native, lost iOS parity, and you’re still paying the platform-view tax on every other WebView flow (HTML5 games, books). One surface patched, structural problem standing.
  6. Avoiding platform views entirely — render the player overlay in native, the rest in Flutter. Feasible for a small app; for a video-centric product it inverts the architecture — a native app that embeds Flutter, not a Flutter app.

Resolution
#

After six weeks of mitigations, the call was structural. The platform-view ANR lived at the intersection of three closed surfaces — Flutter’s composition logic, Android’s HardwareRenderer, and the OEM’s GPU driver blob — and on the devices dominating our emerging-market user base, that intersection was unfixable in any layer I owned.

I migrated Kahf Kids off Flutter to React Native with Expo. The full reasoning — decision matrix, the TV-support failure that compounded this, the three-month AI-assisted delivery, the replacement architecture — is in why we migrated Kahf Kids off Flutter, and the product outcome is in the Kahf Kids Flutter deep dive. Short version: on the same device matrix that was ANR-ing before, the ANR surface disappeared — because a native WebView in React Native’s view tree is a first-class view, not a foreign view composed into a foreign renderer.

That’s not a Flutter-is-bad conclusion. I keep Flutter in production on the NCC app and the Crewlix HR platform because their core flows stay inside Flutter’s canvas — no load-bearing WebView under sustained load. Right tool, right product.

Takeaways
#

If you’re hitting a flutter platform view anr on low-end devices, here’s the diagnostic I’d run, in order:

  1. Pull the Play Console vitals and group ANRs by device + GPU + driver build, not by app version. If the cluster sits on Mali-G52/G57 or PowerVR GE8320 on specific OEM blobs, you’re looking at the failure mode above. Stop debugging your Dart.
  2. Reproduce on real cheap hardware, not an emulator, not a Pixel. A used Redmi Note 11 or Samsung A12 costs less than a day of engineering time. Run a 20-minute WebView session under Perfetto and look for the Raster thread parked on MaliglesWaitFence or the PowerVR equivalent.
  3. Enable Texture Layer Hybrid Composition first. It’s free, it’s the engine’s intended mitigation, and it’ll take the edge off while you decide whether the problem is structural.
  4. Distinguish jank from context-lock. Dropped frames are a tuning problem; a five-second main-thread block on a GPU fence is architectural. Don’t conflate them.
  5. Apply the smell test early. If your core flow is a flutter webview mali gpu scenario — video-heavy, map-heavy, camera-heavy — on a flutter low end device user base, treat platform views as a structural risk before you write the first line of Dart.

The hardest part was admitting the problem wasn’t fixable in the layer I owned. I patched the video flow three times before I accepted that the issue lived below the app layer. Once I accepted that, the decision took an afternoon. That diligence is what I lay out in our full comparison of Flutter vs React Native vs Expo, and it’s the work I do with founders before a line of production code is written.

The migration that came out of this investigation is documented in migrating Kahf Kids off Flutter, and the Flutter architecture it left behind — clean-arch layers, Riverpod, the WebView overlay that sat in the hot path — is in the Kahf Kids Flutter architecture deep dive. The Expo/React Native rebuild that replaced it — including the TV spatial-navigation path — is in building the Kahf Kids RN/Expo architecture.

If you’re hitting the same class of Flutter platform-view ANR — a WebView, map, or camera flow freezing on Mali or PowerVR handsets you can’t reproduce on a Pixel — get in touch and I’ll tell you straight whether it’s patchable in your layer or structural.