
- Published on
- ·5 min read
Your Flutter App Is Slow — Here Are the Mistakes You're Probably Making
- Authors

- Name
- Bert / DOTUNE
- Developer
Back when I was still doing backend, I once compiled our company's Android app on my own phone. A colleague glanced over and said, "Wow, that's smooth."
At the time, iOS devices were mostly still on 60Hz displays. Even the person responsible for this app had apparently never seen it run truly fluidly.
But you see people on Zhihu claiming Flutter is laggy, constantly dragging out the Xianyu app as an example of rendering stutter. Having used Xianyu myself, I haven't observed any rendering issues — so I think there's some misunderstanding here.
That said, Flutter does have a few spots where one slip-up produces visible jank. Here are the most common ones I've hit, in order of priority.
1. Three Basic Mistakes (Quick Hits)
setState() at the Top, Rebuilding Everything
// ❌ setState at the Scaffold level → rebuilds everything below
// ✅ Extract the changing part, mark the rest const
Flutter is declarative: when you call setState(), the Framework rebuilds the entire subtree from that node downward. Putting setState() at the Scaffold level tells Flutter "the whole page is dirty, redraw it all" — when all you wanted was to toggle a tab highlight.
The fix is simple: extract the part that changes into its own StatefulWidget. Mark everything else const. Flutter sees const widgets and skips them entirely during rebuilds.
Not Using const
// ❌ Text('Hello') ← new on every build
// ✅ const Text('Hello')
Flutter's widget tree has three layers: Widget (configuration) → Element (lifecycle management) → RenderObject (actual painting). Adding const tells Flutter this widget's configuration never changes, so the Framework can reuse the previous frame's result at the Element layer — completely skipping the rebuild.
const costs nothing. It has no side effects. Lint rules can auto-fix it. And honestly, Flutter's own tooling has been enforcing this at the language level for a while now.
Measuring Performance in Debug Mode
// ❌ flutter run (debug mode, JIT-compiled, 2-10x slower than release)
// ✅ flutter run --profile (AOT-compiled, near-release performance)
Debug mode is Flutter's default. It runs JIT-compiled, with debug assertions and hot reload infrastructure attached — which makes it 2-10x slower than release. The stores block debug builds, so CI/CD won't accidentally ship them. But plenty of people skip flutter run --profile even during local testing and spend an entire day optimizing jank that doesn't exist in production.
One line to remember: lock down const, push setState down, and profile with --profile. Do these three first, and most "mysterious stutter" disappears.

2. You're Writing Functions, Not Widgets (This One Matters)
// ❌ A lot of people write this
Widget _buildHeader() => Padding(
padding: EdgeInsets.all(8),
child: Text('Header'),
);
// ✅ Should be this
class Header extends StatelessWidget {
const Header({super.key});
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.all(8),
child: Text('Header'),
);
}
}
Why does this matter so much?
- Functions can't be
const→ they rebuild every time - DevTools' Widget Tree doesn't show functions → you can't track how often they rebuild
- Hot reload doesn't work on functions → changes are invisible until full restart
- Flutter's framework-level optimizations (element reuse) don't apply to functions at all
3. Expensive Operations Inside build() (This One Also Matters)
// ❌ Runs on every rebuild
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width; // Keyboard pop triggers this
final items = jsonDecode(hugeJsonString); // Blocks UI thread
final dropdownItems = list.map((e) => // 1000 items
DropdownMenuItem(value: e, child: Text(e))
).toList();
return ...;
}
// ✅ Cache / precompute / narrow the scope
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context); // Only triggers on size changes
final dropdownItems = _cachedItems; // Built once in initState
return ...;
}
build() gets called way more often than you think — keyboard pop-ups, screen rotations, animation frames, even a tiny setState can all trigger it. If you put expensive work inside build(), it runs constantly and eats your UI thread.
The most common offenders: JSON parsing, mass list maps/filters, MediaQuery.of(context).size (one keyboard event and the whole page recalculates). One rule: build() should only contain "assemble UI from existing state" logic. Everything else goes elsewhere. Parsing → initState. Caching → fields. Use MediaQuery.sizeOf instead of MediaQuery.of(context).size to limit rebuild scope.
4. Forgetting dispose() — Memory Creep
// ❌ These will never be freed
class _MyPageState extends State<MyPage> {
final _scrollController = ScrollController();
final _animationController = AnimationController(...);
final _streamSubscription = someStream.listen((_) {});
// ❌ No dispose()!
}
// ✅
void dispose() {
_scrollController.dispose();
_animationController.dispose();
_streamSubscription.cancel();
super.dispose();
}
Controllers, StreamSubscriptions, Animations — these keep consuming resources after creation until you explicitly release them. Flutter won't clean them up for you. A Widget being removed from the tree doesn't mean its resources are freed.
If your State creates a ScrollController or AnimationController without a matching dispose(), those objects stay alive after the page is gone. Memory builds up. The app gets slower the longer a user scrolls around.
Watch out especially for: BuildContext held inside async callbacks, GlobalKey reuse, infinite push-without-pop navigation stacks. All memory leak sources.
Bonus: Third-Party Package Performance
pub.dev packages aren't automatically safe to use. Honestly, I've had to fix or work around upwards of 80% of widget packages before they were production-ready. Before adding anything to your real project, spend time validating it in a minimal demo. It's always worth it.