Performance is crucial for mobile apps. A slow, laggy app will frustrate users and hurt your app store ratings. As Flutter developers, we sometimes unknowingly introduce performance bottlenecks that can significantly impact user experience.
In this post, I'll share 10 common Flutter performance mistakes I've encountered in real projects and how to fix them. These optimizations can make the difference between a smooth, responsive app and one that users immediately uninstall.
1. Building Widgets in the build()
Method
The Mistake:
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// Creating new widget instances every rebuild
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.purple],
),
),
child: Text('Hello'),
),
],
);
}
}
The Fix:
class MyWidget extends StatelessWidget {
// Define widgets as static final or const
static final _gradient = LinearGradient(
colors: [Colors.blue, Colors.purple],
);
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
decoration: BoxDecoration(gradient: _gradient),
child: const Text('Hello'),
),
],
);
}
}
Why It Matters: Creating new widget instances on every rebuild is expensive. Use const
constructors and extract complex widgets into separate classes or static variables.
2. Not Using const
Constructors
The Mistake:
Column(
children: [
Text('Static text'),
Icon(Icons.star),
SizedBox(height: 20),
],
)
The Fix:
Column(
children: [
const Text('Static text'),
const Icon(Icons.star),
const SizedBox(height: 20),
],
)
Why It Matters: The const
keyword tells Flutter that a widget is immutable and can be reused. This prevents unnecessary widget rebuilds and saves memory.
3. Overusing setState()
with Large Widget Trees
The Mistake:
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// Large widget tree
ExpensiveWidget(),
AnotherExpensiveWidget(),
Text('$counter'), // Only this needs to update
YetAnotherExpensiveWidget(),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => counter++),
),
);
}
}
The Fix:
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// These won't rebuild
const ExpensiveWidget(),
const AnotherExpensiveWidget(),
// Extract counter to separate widget
CounterDisplay(counter: counter),
const YetAnotherExpensiveWidget(),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => counter++),
),
);
}
}
class CounterDisplay extends StatelessWidget {
final int counter;
const CounterDisplay({Key? key, required this.counter}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text('$counter');
}
}
Why It Matters: setState()
rebuilds the entire widget tree. Extract frequently changing parts into separate widgets to minimize rebuilds.
4. Using Opacity
for Hiding Widgets
The Mistake:
Opacity(
opacity: isVisible ? 1.0 : 0.0,
child: ExpensiveWidget(),
)
The Fix:
// Option 1: Use Visibility
Visibility(
visible: isVisible,
child: ExpensiveWidget(),
)
// Option 2: Conditional rendering
if (isVisible) ExpensiveWidget(),
Why It Matters: Opacity
still renders the widget even when invisible, consuming resources. Use Visibility
widget or conditional rendering instead.
5. Not Implementing ListView.builder
for Large Lists
The Mistake:
ListView(
children: List.generate(1000, (index) => ListTile(
title: Text('Item $index'),
)),
)
The Fix:
ListView.builder(
itemCount: 1000,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
)
Why It Matters: ListView.builder
creates widgets on-demand as they become visible, dramatically improving performance for large lists.
6. Excessive Use of ClipRRect
and ClipPath
The Mistake:
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Container(
color: Colors.blue,
child: Text('Clipped content'),
),
)
The Fix:
Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10),
),
child: Text('Content'),
)
Why It Matters: Clipping operations are expensive. Use Container
with BoxDecoration
for simple rounded corners instead of ClipRRect
.
7. Loading Large Images Without Optimization
The Mistake:
Image.asset('assets/large_image.jpg') // 4MB image
The Fix:
Image.asset(
'assets/large_image.jpg',
width: 200,
height: 200,
fit: BoxFit.cover,
cacheWidth: 400, // Resize image in memory
cacheHeight: 400,
)
Why It Matters: Large images consume significant memory. Always specify dimensions and use cacheWidth
/cacheHeight
to resize images in memory.
8. Not Using RepaintBoundary
for Expensive Widgets
The Mistake:
CustomPaint(
painter: ComplexChartPainter(),
child: Container(
// Complex drawing operations
),
)
The Fix:
RepaintBoundary(
child: CustomPaint(
painter: ComplexChartPainter(),
child: Container(
// Complex drawing operations
),
),
)
Why It Matters: RepaintBoundary
isolates expensive painting operations, preventing them from affecting other widgets during repaints.
9. Synchronous Operations on the Main Thread
The Mistake:
void loadData() {
// Blocking operation on main thread
String data = heavyComputation();
setState(() {
// Update UI
});
}
The Fix:
Future<void> loadData() async {
// Move heavy computation to isolate
String data = await compute(heavyComputation, inputData);
setState(() {
// Update UI
});
}
String heavyComputation(dynamic input) {
// Heavy computation here
return result;
}
Why It Matters: Heavy computations on the main thread cause UI freezing. Use compute()
function to run expensive operations in isolates.
10. Overusing AnimatedBuilder
Instead of Specific Animated Widgets
The Mistake:
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _controller.value,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
);
},
)
The Fix:
ScaleTransition(
scale: _controller,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)
Why It Matters: Specific animated widgets like ScaleTransition
, FadeTransition
, and SlideTransition
are more optimized than generic AnimatedBuilder
.
Performance Monitoring Tips π©
To catch performance issues early:
- Use Flutter Inspector to identify widget rebuild issues
-
Enable performance overlay with
flutter run --profile
-
Use
Timeline
in DevTools to analyze frame rendering times - Monitor memory usage in DevTools Memory tab
- Test on real devices, especially lower-end ones
Conclusion π€―
Performance optimization is an ongoing process. These 10 mistakes are common but easily fixable with the right approach. Remember to profile your app regularly and always test on real devices.
The key is to think about widget rebuilds, memory usage, and main thread blocking operations. By avoiding these common pitfalls, you'll create Flutter apps that feel smooth and responsive.
Have you encountered any of these performance issues in your Flutter projects? Share your experiences in the comments below!
Top comments (0)