跪拜 Guibai
← All articles
Front-end · Flutter · Android

Flutter TLHC WebView Finally Hits 120 FPS: The Fix Was Rearranging Two Lines

By 恋猫de小郭 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

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.

Summary

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.

Takeaways
— TLHC WebView scrolling jumps from ~70 FPS to 120 FPS after the merged fix.
— Main-window frame sync time falls from ~5.1 ms to ~1.4 ms, a reduction of 3.7 ms.
— The 3.7 ms saving is 44% of the 8.33 ms frame budget on a 120Hz device.
— The fix defers unlockCanvasAndPost to the next main thread message using postAtFrontOfQueue.
— Android HardwareRenderers share a single RenderThread per process, so offscreen draws can starve the main window's sync.
— The PR adds explicit handling for repeated draw(), resizes, backgrounding, destruction, and exceptions in unlockCanvasAndPost.
— A separate optimization resizes the render target before creating the view wrapper, avoiding a wasted 1×1 ImageReader allocation.
— The deferred submission keeps the unlock call on the main thread, just reordered within the message queue.
Conclusions

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.

Concepts & terms
TLHC (Texture Layer Hybrid Composition)
A Flutter hybrid composition mode where native Android Views are first drawn to a texture, then composited by Flutter. It gives better Flutter-side performance but previously suffered from WebView scrolling jank because of RenderThread contention.
RenderThread
Android's GPU rendering thread that executes drawing commands recorded by the main thread. All HardwareRenderers in a process share a single RenderThread, so heavy offscreen draws can block the main window's frame sync.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗