跪拜 Guibai
← All articles
Frontend · Flutter · Android

Flutter's 7-Year-Old Touch Smoother Was Adding a Frame of Latency on Modern iPhones

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

Any Flutter app on iOS is paying a hidden 8–16 ms touch latency penalty on devices where UIKit's input delivery is already stable. This fix reclaims a full frame of responsiveness for scrolling and drag gestures, and the underlying scheduling corrections prevent frame-rate regressions when the old workaround is removed.

Summary

The `SmoothPointerDataDispatcher`, introduced nearly seven years ago to handle irregular touch delivery on the iPhone X, was still buffering 99% of pointer packets to the next VSync on current hardware. UIKit's delivery spread has shrunk from 12 ms to under 1 ms, making the smoother a pure latency penalty. Removing it caused frame rates to halve on some devices, revealing that the smoother had been masking a scheduling defect where the engine's internal `PostTask` calls caused it to miss the current `CADisplayLink` callback.

PR #191368 dismantles three waiting points in the touch pipeline. It replaces the smoother with a default dispatcher, switches to `RunNowOrPostTask` to avoid unnecessary re-posting on the same thread, and makes the `AwaitVSync` call synchronous within the same run-loop turn. A separate fix keeps `CADisplayLink` unpaused during continuous interaction and fires an immediate callback for the first frame from idle, preventing the 30 fps drops seen in earlier experiments.

The contributor, Knopp, also documented a running battle with Gemini Code Assist, which repeatedly flagged a non-existent data race because it failed to recognize that Flutter's iOS embedder merges the platform and UI threads by default.

Takeaways
UIKit's touch delivery spread on modern iPhones is under 1 ms, down from 12 ms on the iPhone X, making the `SmoothPointerDataDispatcher` obsolete.
The smoother was stashing 99% of pointer packets to the next VSync, adding 8.33 ms (120 Hz) or 16.67 ms (60 Hz) of latency to every touch interaction.
Removing the smoother without fixing the VSync waiter caused frame rates to drop from 120 fps to 60 fps, and from 60 fps to 30 fps on some devices.
The fix replaces three `PostTask` hops in the touch-to-frame-request pipeline with synchronous execution within the same run-loop turn.
`AwaitVSync` is now called directly in `Animator::RequestFrame()` instead of being re-posted to the UI task runner.
`CADisplayLink` is kept unpaused during continuous interaction, and the first frame from idle fires an immediate callback instead of waiting for the next tick.
Gemini Code Assist incorrectly flagged a data race on `waiting_for_vsync_` because it did not know that Flutter's iOS embedder merges the platform and UI threads.
Conclusions

A performance optimization that made sense on one hardware generation became a guaranteed latency regression on all subsequent ones, and it survived for seven years because it accidentally papered over a separate scheduling bug.

iOS's scheduler sometimes demotes threads to E-cores when their workload appears too light, which means a well-optimized rendering pipeline can be punished with worse frame pacing than a deliberately heavier one.

The two `PostTask` calls in the touch pipeline were not expensive in CPU time, but they shifted the phase of the `AwaitVSync` call just enough to miss the current display-link dispatch, costing an entire frame.

AI code review tools that lack awareness of platform-specific thread configurations can block valid patches with false positives, forcing contributors to spend time arguing with a bot about architecture it does not understand.

Concepts & terms
E-core / P-core
In Apple Silicon and modern Intel hybrid architectures, Performance-cores (P-cores) handle demanding tasks with higher clock speeds, while Efficiency-cores (E-cores) handle background or low-intensity work to save power. iOS can schedule threads onto E-cores if it deems their workload light.
VSync / CADisplayLink
VSync synchronizes frame rendering with the display's refresh rate. On iOS, `CADisplayLink` provides a callback aligned with the screen's refresh cycle. Missing a callback means waiting for the next one, adding a full frame of latency.
Run-loop turn
A single pass through the event loop where all pending events for a given mode are processed. UIKit's update cycle runs several phases (event dispatch, display-link dispatch, transaction commit) within one run-loop turn without returning to the loop.
SmoothPointerDataDispatcher
A Flutter component that buffers incoming touch packets and dispatches them on the next VSync to smooth out irregular delivery from UIKit. It was designed for the iPhone X era when touch events arrived unevenly across frames.
From the discussion

The discussion veered away from Flutter's touch latency fix. One comment dismisses the article's AI-generated summary with a laugh, and a reply agrees that Gemini underperforms except for image generation. A separate remark speculates about the author's recent departure from Canonical.

The AI-generated summary is poor, though Gemini excels at generating images of women.
The article's author may have recently left Canonical.
See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗