Flutter Performance Tips — Because A Slow App Is A Dead App
Let me paint you a picture.
You’ve built something beautiful. The UI looks great, the features work, the client is happy. You ship it. And then the reviews start coming in.
“App is laggy.” “Scrolling is not smooth.” “Why does this take so long to load?”
And you’re sitting there thinking — but it worked fine on my device.
Performance issues are sneaky. They don’t always show up during development. They show up when real users with real devices and real data start using your app. And by then, the damage to your app’s reputation is already happening.
This post is about the Flutter performance mistakes that are easy to make and easy to fix — once you know what to look for.
1. Use const Wherever You Can
This is the simplest, most impactful performance improvement you can make and it costs you nothing.
When you mark a widget as const, Flutter knows it will never change. It builds it once and reuses it instead of rebuilding it every time the parent rebuilds.
// ❌ This gets rebuilt every time the parent rebuilds
Text('Hello World')
// ✅ This is built once and reused
const Text('Hello World')// ❌ Not const
Container(
padding: EdgeInsets.all(16),
child: Text('Hello'),
)
// ✅ Const
const Padding(
padding: EdgeInsets.all(16),
child: Text('Hello'),
)Flutter will actually warn you when something can be const but isn’t — pay attention to those warnings. They’re not just style suggestions, they’re performance hints.
2. Stop Rebuilding Everything
This is the big one. The one that causes most Flutter performance problems.
Every time setState is called, Flutter rebuilds the widget and all its children. If your entire screen is one big widget tree and you call setState for a tiny change — everything rebuilds. Even the parts that didn't change.
The fix is to make your widgets smaller and more focused.
// ❌ One giant widget — entire screen rebuilds for any state change
class MyScreen extends StatefulWidget {
@override
State<MyScreen> createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
bool isLiked = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
// Huge header widget
// Long list
// Footer
// All of this rebuilds when isLiked changes
IconButton(
icon: Icon(isLiked ? Icons.favorite : Icons.favorite_border),
onPressed: () => setState(() => isLiked = !isLiked),
),
],
);
}
}// ✅ Extract the changing part into its own widget
class LikeButton extends StatefulWidget {
@override
State<LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton> {
bool isLiked = false;
@override
Widget build(BuildContext context) {
// Only THIS widget rebuilds now
return IconButton(
icon: Icon(isLiked ? Icons.favorite : Icons.favorite_border),
onPressed: () => setState(() => isLiked = !isLiked),
);
}
}This is also why state management solutions like Cubit are useful — they give you precise control over exactly which widgets rebuild when state changes.
3. Use ListView.builder For Long Lists
Never use a regular ListView with a list of children for long scrollable content. It builds all items at once, even the ones not visible on screen.
// ❌ Builds all 1000 items at once
ListView(
children: items.map((item) => ListTile(title: Text(item))).toList(),
)
// ✅ Only builds items currently visible on screen
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: Text(items[index]));
},
)ListView.builder is lazy — it only builds what's on screen and a small buffer around it. For a list of 10 items it doesn't matter much. For a list of 500 items it's the difference between smooth scrolling and a slideshow.
Same goes for GridView.builder over GridView with children.
4. Use cached_network_image For Network Images
Every time you use Image.network, Flutter downloads the image from the internet. Every. Single. Time. Even if it just downloaded it five seconds ago.
dart
// ❌ Downloads every time
Image.network('https://example.com/photo.jpg')// ✅ Downloads once, caches it, reuses it
CachedNetworkImage(
imageUrl: 'https://example.com/photo.jpg',
placeholder: (context, url) => const ShimmerWidget(),
errorWidget: (context, url, error) => const Icon(Icons.error),
)cached_network_image stores downloaded images locally. The second time the image is needed it loads from cache — instantly, no network call. Your app feels faster, your users use less data, everyone wins.
5. Avoid Doing Heavy Work In Build
The build method gets called a lot. Every rebuild calls it. If you're doing expensive operations inside build — parsing data, running calculations, formatting strings in complex ways — that's happening on every single rebuild.
// ❌ Parses and sorts the list every rebuild
@override
Widget build(BuildContext context) {
final sortedItems = items
.where((item) => item.isActive)
.toList()
..sort((a, b) => a.name.compareTo(b.name));return ListView.builder(
itemCount: sortedItems.length,
itemBuilder: (context, index) => ListTile(title: Text(sortedItems[index].name)),
);
}
// ✅ Do it once, store the result
late final List<Item> sortedItems;
@override
void initState() {
super.initState();
sortedItems = items
.where((item) => item.isActive)
.toList()
..sort((a, b) => a.name.compareTo(b.name));
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: sortedItems.length,
itemBuilder: (context, index) => ListTile(title: Text(sortedItems[index].name)),
);
}Build should be as light as possible. Compute once, display many times.
6. Use RepaintBoundary For Complex Animations
When a widget animates, Flutter repaints it and everything around it. For complex animations in a busy UI this can cause jank.
Get Shivani Bagal’s stories in your inbox
Join Medium for free to get updates from this writer.
RepaintBoundary tells Flutter to isolate a widget into its own paint layer so its animations don't trigger repaints in the rest of the tree.
RepaintBoundary(
child: AnimatedWidget(), // only this repaints during animation
)Use this around widgets that animate independently from the rest of your UI — things like loading spinners, Lottie animations, or any custom animated widget.
7. Avoid Opacity Widget For Simple Cases
Opacity widget creates a new compositing layer and is more expensive than you'd think for something that sounds simple.
// ❌ More expensive than it looks
Opacity(
opacity: 0.5,
child: MyWidget(),
)// ✅ Use color opacity instead where possible
Container(
color: Colors.blue.withOpacity(0.5),
)
// ✅ Or AnimatedOpacity if you need animation
AnimatedOpacity(
opacity: isVisible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
child: MyWidget(),
)For static partial transparency on a complex widget, Opacity is fine. For frequent changes or animations, prefer other approaches.
8. Use the Flutter DevTools
You can write about performance tips all day but the real skill is knowing how to find performance problems in your own app.
Flutter DevTools is built into both VS Code and Android Studio. The Performance tab shows you frame rendering times — Flutter targets 60fps which means each frame should render in under 16ms. Frames that take longer show up in red. That’s where your performance problem is.
The Widget Rebuild tab shows you which widgets are rebuilding and how often. If a widget is rebuilding 50 times per second and it doesn’t need to — that’s your problem right there.
Run your app in profile mode for performance testing:
flutter run --profileDebug mode adds overhead that makes performance look worse than it is. Profile mode gives you accurate numbers.
The Quick Checklist
Before shipping any Flutter app, go through this:
- ✅ const on every widget that doesn’t change
- ✅ ListView.builder for any list that could be long
- ✅ cached_network_image for all network images
- ✅ No heavy operations inside build methods
- ✅ Small focused widgets instead of one giant rebuild target
- ✅ RepaintBoundary around complex independent animations
- ✅ Tested in profile mode on a real device
You don’t need to optimize everything from day one. Premature optimization is its own problem. But knowing these patterns means you build performant apps by default — and the ones that aren’t, you know exactly where to look.
Because a fast app feels good. A slow app feels broken. And users can’t always tell you why it feels slow — they just know it does.
Don’t give them the chance.
— Shivani Bagal 🤍
“Performance issues hiding in your app? Open Flutter DevTools, go to the Performance tab, and look for the red frames. That’s always where the problem is.”








