setState coalescing prevents per-update frames during rapid UI changes
0 reputation · 11 Mar 2023, 11:37 UTC
0 reputation · 11 Mar 2023, 11:37 UTC
When multiple setState() calls are issued in quick succession — for example, from a high-frequency sensor stream or rapid user interactions — the widget tree rebuilds only once per frame instead of once per call. The UI appears to lag behind the data source because updates are merged into a single build pass.
Flutter’s UI isolate processes setState() by marking widgets dirty and requesting a frame via the VSync callback. Calls that arrive within the same event-loop tick are coalesced, so build() runs once for the next frame. This behavior is intentional but can look like latency when the application expects a visible frame for each logical update.
setState() during a build; profile/release builds may defer silently.addPostFrameCallback() ordering can create additional frame delays.setState() coalescing in the current stable Flutter release, and does it differ between debug and profile builds?35275 reputation · 11 Mar 2023, 12:11 UTC
Flutter’s setState() does not promise one build per call. It marks the target State dirty and asks the scheduler for a frame. Calls that arrive in the same event-loop tick are normally coalesced into a single build pass for the next frame. This is intentional; Flutter cannot run more than one build per frame.
Version assumption: current stable Flutter. Exact scheduler internals can shift between minor versions, so verify against your installed release.
Confirmed: SchedulerBinding owns frame scheduling. setState() adds the widget to the dirty list and requests a frame. Multiple requests before the next frame start collapse into one build phase. Debug builds assert against reentrant setState() during an active build; profile and release builds may defer silently, but the coalescing contract is unchanged.
Likely explanation: your sensor stream or interaction handler is issuing several setState() calls between VSync boundaries. The UI isolate sees one dirty set and renders one frame, so intermediate logical states never reach the screen. GPU raster backpressure can cause separate jank, but it is not the cause of coalescing.
setState() runs.setState() after the current frame ends.setState() again from inside a build. Schedule from the event handler, timer, or stream listener that owns the logical update.SchedulerBinding.instance.addPostFrameCallback((_) {
setState(() {
// apply next logical update
});
});
Open DevTools Performance and record while reproducing the rapid updates. Correlate these timeline events:
| Event | What it tells you |
|---|---|
| Frame Start | Marks the beginning of a new UI frame. |
| Build | Runs once per frame for dirty widgets. Multiple setState() calls before this point indicate scheduler coalescing. |
| Raster | Render-thread work. If Build finishes quickly but Raster is late, the bottleneck is GPU or render-thread backpressure, not Dart scheduling. |
If several setState() timestamps fall between one Frame Start and the next Build, you are seeing coalescing. If Build is immediate but Raster trails, investigate render-thread load instead.
Are the rapid setState() calls posted from the UI isolate or a background isolate? This changes whether you need to optimize isolate message passing in addition to frame scheduling.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 11 Mar 2023, 22:15 UTC
One follow-up on question 3, which the main answer skips: the DevTools Performance view splits each frame into a UI-thread timeline (build, layout, paint) and a raster-thread timeline. Coalescing appears as a single Frame event containing one Build phase regardless of how many setState() calls preceded it — several logical updates with only one Frame event means scheduling merged them.
Raster backpressure looks different: the UI slice stays short while the Raster slice exceeds the frame budget, roughly 16.7 ms at 60 Hz and about half that on 120 Hz displays. Consistently over-budget raster time means dropped frames are a rendering problem, not a setState() problem.
For question 2, the concrete serialization primitive is SchedulerBinding.instance.endOfFrame: awaiting it before issuing the next update pins each logical update to its own frame, assuming raster keeps up. Verify the exact API name and behavior against your installed Flutter release.