Flutter Performance Optimization: Finding and Fixing Jank
Jank Has Specific, Diagnosable Causes — Stop Guessing
"The app feels laggy" is not a starting point for optimization — it's a symptom. Flutter DevTools gives you exact frame timing data, and almost every real-world jank case traces back to one of a small number of patterns.
Step 1: measure before you guess
Open the Performance view in Flutter DevTools and enable the frame rasterizer stats. You're looking for frames that exceed 16ms (60fps) or 8ms (120fps). DevTools will tell you whether the bottleneck is the UI thread (build/layout) or the raster thread (painting) — and the fix is completely different depending on which.
UI-thread jank: expensive builds
// Bad: rebuilds this entire expensive widget every time the parent rebuilds
Widget build(BuildContext context) {
return Column(children: [
ExpensiveChart(data: chartData),
SimpleCounter(count: count),
]);
}
// Better: const constructors + isolating rebuilds
Widget build(BuildContext context) {
return Column(children: [
const ExpensiveChart(data: chartData), // won't rebuild if inputs are unchanged
SimpleCounter(count: count),
]);
}Mark widgets const wherever possible — it lets Flutter skip rebuilding them entirely when nothing they depend on changed. This alone fixes a surprising fraction of jank reports.
Raster-thread jank: overdraw and expensive paints
- Opacity is expensive — animating
Opacityforces a full re-composite every frame. PreferAnimatedOpacityor, better, pre-baked assets where possible. - Clipping is expensive —
ClipRRect,ClipPathon large widgets or lists cost real raster time. Use them sparingly, and check the "Highlight Repaints" DevTools overlay to see what's clipping unnecessarily. - Shader compilation jank — the first time a custom shader/gradient/blur runs, Flutter compiles it, causing a visible stutter. Use
ImpellerEnabledengine improvements and pre-warm shaders where this is a known issue (common right after cold start).
List performance: the most common offender
// Bad: builds all items eagerly
ListView(children: items.map((i) => ItemTile(i)).toList());
// Good: lazily builds only visible items
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ItemTile(items[index]),
);This single change — ListView to ListView.builder — is responsible for more jank fixes in real Flutter codebases than almost anything else on this list.
A practical checklist
- Use
ListView.builder/GridView.builderfor anything beyond a handful of items. - Mark widgets
constwherever their inputs don't change. - Avoid animating
Opacityand heavyClip*widgets; profile with "Highlight Repaints" before assuming they're the cause. - Move genuinely CPU-heavy work (JSON parsing of large payloads, image manipulation) off the main isolate with
compute(). - Re-measure after every fix — DevTools timing data, not vibes, is the only reliable signal that a change actually helped.