Engineering
Flutter Performance: Hitting 60fps on Mid-Range Devices
Your app is smooth on the test device and janky in the hands of real users. That gap is where most Flutter performance work actually lives.
Profile on the worst device you support
Everything that follows is secondary to this. A Flutter app profiled only on a current flagship tells you almost nothing — those devices have enough headroom to hide genuinely bad code. We keep a drawer of three-to-four-year-old mid-range Androids, and that is where release builds get signed off.
Always profile a release or profile build. Debug builds run Dart in JIT with assertions enabled and are several times slower. Performance conclusions from a debug build are worthless.
Know which thread is dropping the frame
Flutter's DevTools timeline shows two bars per frame: UI and raster. Which one is over budget tells you what to fix, and they need completely different treatment.
- UI thread over budget — your Dart is too slow. Expensive build methods, work in
build(), JSON parsing on the main isolate, rebuilding too much of the tree. - Raster thread over budget — you're asking the GPU for too much. Large blurs, unnecessary opacity layers, clipping with anti-alias-and-save-layer, oversized images.
The build-cost fixes, in order of return
- Use const everywhere it's legal. A const widget is canonicalised at compile time; Flutter sees an identical instance on rebuild and skips the subtree entirely. Turn on the lint and fix every hit.
- Push setState as far down as it goes. Calling setState in a screen widget rebuilds the whole screen. Extract the changing part into its own small stateful widget so the rebuild is contained.
- Never build long lists eagerly.
ListView(children: [...])builds every child immediately.ListView.builderbuilds only what's visible. This single change has rescued more screens than any other. - Get work out of build(). build() can run 60 times a second. No parsing, no sorting, no file access, no creating controllers.
- Move heavy work to an isolate. Anything over a few milliseconds — large JSON, image processing, crypto — belongs in
compute()rather than blocking the frame.
The raster-cost fixes
- Opacity is not free.
Opacityallocates an offscreen buffer. For a fade,FadeTransitionorAnimatedOpacityis cheaper; for a static translucent colour, just use a colour with alpha. - Isolate animating subtrees with
RepaintBoundary, so a small spinner doesn't force the whole screen to repaint. Used carelessly it costs memory — measure, don't sprinkle. - Decode images at display size. A 4000px photo drawn into a 200px avatar decodes at full resolution. Set
cacheWidth/cacheHeightor resize server-side. - Treat blur as a luxury.
BackdropFilteris one of the most expensive things you can put on screen, and it's worst on the cheap GPUs you most need to support.
Set budgets before you write code
On client projects we agree numbers during discovery and write them into the acceptance criteria alongside the features. Typical targets:
- Cold start under 2 seconds on the reference mid-range device.
- No frame over 16ms during scroll on the three primary screens.
- Crash-free sessions above 99.9%.
- Release bundle under an agreed ceiling, checked in CI so a careless asset can't quietly add 4MB.
Numbers in the contract change the conversation. "It feels a bit slow" is an opinion you can argue with; "the checkout screen drops 22 frames on a Redmi Note" is a defect with an owner and a deadline.
Building something with Flutter?
We'll tell you honestly whether Flutter is right for it — including when it isn't. Free 30-minute call, no pitch deck.