Flutter gives you a lot out of the box. A fast rendering engine, a reactive UI model, and 60fps as the default expectation. But it is surprisingly easy to write Flutter code that quietly throws all of that away.
These five mistakes show up in real Flutter codebases constantly. None of them are obvious when you write them. All of them are fixable once you know what to look for.
1. Calling setState on a parent when only a child needs to rebuild
This is the Flutter equivalent of the giant React component problem, and it is probably the most common performance issue in Flutter apps. When you call setState on a parent widget, the entire subtree below it rebuilds. If that subtree contains complex layouts, images, or lists, you are doing a lot of unnecessary work on every state change.
The bad pattern:
class ParentWidget extends StatefulWidget {
@override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
HeavyListWidget(), // rebuilds every time counter changes
Text('$counter'),
ElevatedButton(
onPressed: () => setState(() => counter++),
child: Text('Increment'),
),
],
);
}
}
The fix:
Extract the part that actually changes into its own StatefulWidget. HeavyListWidget never needs to rebuild when the counter changes, so it should not be in the same setState scope.
class CounterWidget extends StatefulWidget {
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('$counter'),
ElevatedButton(
onPressed: () => setState(() => counter++),
child: Text('Increment'),
),
],
);
}
}
Now HeavyListWidget lives outside this scope entirely and never rebuilds unless it needs to.
2. Building widgets inside the build method that should be constants
Flutter is declarative, which means build() gets called frequently. Anything you construct inside it gets reconstructed on every rebuild. If you are creating widgets that never change, declaring them as const tells Flutter to reuse the existing instance instead of building a new one.
The bad pattern:
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: EdgeInsets.all(16),
child: Text(
'Welcome',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
],
);
}
The fix:
@override
Widget build(BuildContext context) {
return const Column(
children: [
Padding(
padding: EdgeInsets.all(16),
child: Text(
'Welcome',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
],
);
}
The const keyword here is not just a style preference. Flutter skips the widget entirely during reconciliation if it is const and nothing about it has changed. Enable the prefer_const_constructors lint rule in your analysis_options.yaml and let the analyzer flag every missed opportunity automatically.
3. Using ListView instead of ListView.builder for long lists
When you use ListView with a children list, Flutter builds every single item upfront regardless of whether it is visible on screen. For short static lists this is fine. For anything that scrolls, it is a serious problem.
The bad pattern:
ListView(
children: items.map((item) => ItemCard(item: item)).toList(),
)
If items has 500 entries, Flutter builds 500 ItemCard widgets before the user even sees the list.
The fix:
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ItemCard(item: items[index]);
},
)
ListView.builder is lazy. It only builds widgets that are currently visible plus a small buffer. The performance difference on long lists is significant and very easy to measure in DevTools.
For lists where items have varying heights, use ListView.builder with a cacheExtent value tuned to your typical item height to control how aggressively Flutter pre-builds off-screen items.
4. Performing heavy work directly inside build()
The build method runs on the UI thread and should be as fast as possible. Sorting a list, filtering data, parsing a string, running a computation, none of that belongs inside build(). Every rebuild triggers it again, and if it is slow, your frame rate pays for it.
The bad pattern:
@override
Widget build(BuildContext context) {
final sorted = items.where((i) => i.isActive).toList()
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
return ListView.builder(
itemCount: sorted.length,
itemBuilder: (context, index) => ItemCard(item: sorted[index]),
);
}
Every rebuild refilters and resorts the entire list.
The fix:
late List<Item> _sortedItems;
@override
void initState() {
super.initState();
_sortedItems = _computeSortedItems();
}
List<Item> _computeSortedItems() {
return items.where((i) => i.isActive).toList()
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
}
Compute once in initState and only recompute when the source data actually changes. If the computation is expensive enough, push it to an isolate so it runs off the UI thread entirely.
5. Not using RepaintBoundary to isolate expensive widgets
Flutter repaints widgets in layers. By default, an animation or a frequent update in one part of the screen can trigger repaints in completely unrelated widgets nearby. RepaintBoundary tells Flutter to isolate a widget into its own layer so its repaints stay contained.
The bad pattern:
Stack(
children: [
BackgroundMapWidget(), // expensive, static
AnimatedMarkerWidget(), // updates 60 times per second
],
)
Every frame of the animation repaints the entire Stack including the background map.
The fix:
Stack(
children: [
RepaintBoundary(
child: BackgroundMapWidget(),
),
AnimatedMarkerWidget(),
],
)
Now the map has its own layer. The animation repaints only the marker layer and the map is untouched. This is especially valuable for anything with continuous animations running alongside complex static UI maps, charts, video players, and similar heavy widgets.
You can verify it is working by opening the Flutter DevTools, enabling "Highlight repaints", and watching which parts of the screen flash on each frame. If the map stops flashing, RepaintBoundary is doing its job.
Wrapping up
Flutter is fast by design, but the framework cannot protect you from all of these patterns on its own. setState scope, const widgets, lazy lists, build method purity, and repaint isolation are four habits that separate apps that feel smooth from apps that feel like they are fighting the framework.
The Flutter DevTools Performance tab and the Widget Rebuild tracker are your best tools for catching these in practice. Run a profile build, not a debug build, when profiling the numbers will be completely different.
If this was useful, I write about Flutter, React, and frontend engineering regularly. Follow along for more.

Top comments (0)