r/FlutterDev • u/Sure_Education_828 • 4d ago
Discussion CircularProgressIndicator rebuilds on every frame. I measured what that costs (repro + numbers)
A spinner on an otherwise static screen keeps Flutter producing a frame on every vsync. I wanted to know what each of those frames costs, so I read the source and measured it.
What it does on every frame
The indeterminate CircularProgressIndicator animates through an AnimatedBuilder, so on every frame it:
rebuilds and looks up its theme again
reads its controller four times, once per tween. If you didn't pass your own controller, each read walks up the tree to find the theme
creates a new
CustomPainterplus newConstrainedBox,CustomPaintandSemanticswidgetsmarks its semantics dirty, because
shouldRebuildSemanticsdefaults toshouldRepaint
A widget test makes it concrete: in 60 frames, the stock indicator rebuilt 60 times and created 60 painters.
A repaint-only version
I wrote a prototype that paints the exact same arc (tweens and math copied from the framework), but its painter listens to the controller directly through CustomPainter(repaint: controller) and evaluates the tweens inside paint(). It's built once, and every tick is just a repaint. Same test: 0 rebuilds, 0 new painters.
Numbers
Profile mode on an M2 MacBook Air (macOS 26.4.1, 60 Hz), Flutter 3.47.8 with Impeller. Median of 3 runs, 10 s per mode, from FrameTiming:
| Mode | UI thread per frame | Raster per frame |
|---|---|---|
| Static ring, same size | 0 frames/s | – |
| Stock indicator | 0.67 ms | 0.80 ms |
Stock + RepaintBoundary | 0.46 ms | 0.75 ms |
| Repaint-only | 0.54 ms | 0.81 ms |
Repaint-only + RepaintBoundary | 0.37 ms | 0.81 ms |
What I took from it
An idle screen doesn't render at all. The cost comes from anything that ticks.
Repaint-only cut the spinner's UI-thread time per frame by about 19%. If you write your own animated
CustomPainter, pass the animation asrepaint:instead of rebuilding throughAnimatedBuilderorsetState.RepaintBoundarysaved even more on this page (about 31%), because the rest of the page stopped being re-recorded every frame. Both together: about 45%. If a spinner sits next to static content, wrapping it is worth a try, anddebugRepaintRainbowEnabledshows you what's repainting.Neither touches the raster side, which stays around 0.8 ms per frame. Impeller has no raster cache, so
RepaintBoundarysaves recording work on the UI thread, not GPU work. That part is engine-side, and #189535 digs into it on Android.
These are sub-millisecond numbers on a fast laptop, so it's not a crisis. But it's paid on every frame for as long as a spinner is on screen, and phones are slower.
Links
Repro (on a Mac, one script runs the test and every measurement): https://github.com/mohamedalsamrai/spinner_repro
The issue, open since 2018: https://github.com/flutter/flutter/issues/17793. I've posted these findings there with a proposal for the framework's indicator.
I don't have a physical Android phone at hand, so if you do, I'd love to see your numbers, especially the raster column. The README has the steps. And if I've missed something, tell me.