Flutter TLHC WebView Finally Hits 120 FPS: The Fix Was Rearranging Two Lines
Flutter apps that embed WebViews no longer have to choose between native view compatibility and smooth scrolling. The 44% frame-budget saving on 120Hz devices removes the main objection to TLHC mode, and the deferred-submission pattern is a useful reference for any platform where a shared render thread is a bottleneck.
TLHC mode has always been a performance trap for Flutter WebViews. Because the native WebView is drawn into a texture that Flutter then composites, every frame must record Android View commands, lock an offscreen Surface, and submit it via unlockCanvasAndPost. That submission triggers Android's shared RenderThread, and when the WebView scrolls fast, it grabs the RenderThread before the main window's frame sync. The result was ~70 FPS scrolling on a Pixel 9, with main-window sync alone eating ~5.1ms per frame.
The merge of flutter/flutter#195861 fixes this by deferring the unlockCanvasAndPost call. Instead of submitting inside PlatformViewWrapper.draw(), the new code stores the pending frame and posts a Runnable to the front of the main thread message queue via postAtFrontOfQueue. The main window finishes its own sync first, then the WebView's offscreen frame is submitted, and Flutter gets its texture update. Frame sync drops to ~1.4ms, and scrolling runs at 120 FPS on 120Hz devices.
The PR also guards against edge cases like multiple draw() calls, resizes, backgrounding, destruction, and exceptions in unlockCanvasAndPost. A separate change avoids allocating a 1×1 buffer before resizing the render target, which trims initialization overhead. The core fix is small, but it removes the last reason many Flutter developers avoided TLHC for embedded WebViews.
Delaying a single canvas submission yields a 44% frame-budget saving, showing how much hidden cost a poorly timed offscreen draw can impose on a shared render thread.
The author's concern about new states (locked canvas pending submission) is warranted; the PR's defensive edge-case handling suggests this pattern may need continued maintenance as Flutter's platform view code evolves.
The 1×1 buffer optimization is minor in impact but reveals that Flutter's hybrid composition initialization path had wasteful allocations that could be trimmed once the rendering order was rethought.