Most apps are lists. A list of messages a list of photos or a list of tasks. The difference between an app that feels good and one that feels bad often comes down to how its lists scroll.
Jank is the enemy. It’s that stutter you feel when you flick your thumb to scroll and the screen can’t keep up. It breaks the illusion of a fluid interface and makes your software feel cheap. In Flutter the most common cause of jank is building lists incorrectly.
The most intuitive way to build a list is often the wrong way. A beginner might take a list of data map it to a list of widgets and put them all inside a Column
wrapped in a SingleChildScrollView
.
// Don’t do this for long lists.
SingleChildScrollView(
child: Column(
children: items.map((item) => MyListItem(item: item)).toList(),
),
)
This seems logical. But it’s eager. It tells Flutter to build every single MyListItem
at once before the user sees anything. If you have 10 items it might be fine. If you have 1000 items your app will freeze or crash. It’s wasteful. You’re building hundreds of widgets that aren’t even on the screen.
This is the programming equivalent of cooking an entire week’s worth of meals every time you feel slightly hungry.
The correct way to build long lists in Flutter is to be lazy. Only build what you need when you need it. Flutter gives you the perfect tool for this ListView.builder
.
// Do this instead.
ListView.builder(
// The total number of items in the list.
itemCount: items.length,
// A function that builds each item.
// It’s only called for items that are visible.
itemBuilder: (context, index) {
return MyListItem(item: items[index]);
},
)
The itemBuilder
function is the core of this pattern. Flutter calls it only for the items it needs to draw on the screen. As you scroll down and new items are about to appear Flutter calls itemBuilder
for those new items just in time. As items scroll off the screen Flutter destroys them to reclaim memory.
This simple change is often the most significant performance improvement you can make to your app.
Using ListView.builder
is not a magic bullet. It solves the problem of building too many widgets at once. But it doesn’t solve the problem of building a single widget that is too slow to build.
Your build methods should be pure functions of state. They should do no real work.
Remember the itemBuilder
is called every time a new item needs to be drawn. This happens a lot during a fast scroll. If each call to your item’s build
method takes even a few milliseconds too long you will get jank.
What makes a build
method expensive?
All this work should be done before the build method is ever called. When you get your data from the network or a database process it into a simple model object. The build method should do nothing more than take that model and map its properties to widgets. It should be a simple declarative transformation not a place for computation.
One of the simplest and most effective optimizations is using the const
keyword. If you have a widget in your list item that never changes mark its constructor as const
.
// Slow: an icon that gets rebuilt unnecessarily.
Icon(Icons.star)
// Fast: a constant icon that Flutter can reuse.
const Icon(Icons.star)
When Flutter sees a const
widget it knows it never has to rebuild it. It can create it once and reuse that same instance forever. This saves CPU time. Sprinkle const
liberally throughout the static parts of your widget tree. It’s a free performance win.
Another common issue is state. Imagine your list item has a button that can be favorited. The naive approach might be to call a method that rebuilds the entire list screen when one item is favorited. This is terribly inefficient.
The state for that one item should be contained within the item itself. Wrap your list item’s widget in its own StatefulWidget
or use a micro state management solution. When the favorite button is tapped only that single list item should rebuild not the entire list.
// Each list item manages its own state.
class MyListItem extends StatefulWidget {
final Item item;
const MyListItem({Key? key, required this.item}) : super(key: key);
@override
_MyListItemState createState() => _MyListItemState();
}
class _MyListItemState extends State<MyListItem> {
bool _isFavorited = false;
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(widget.item.name),
trailing: IconButton(
icon: Icon(_isFavorited ? Icons.star : Icons.star_border),
onPressed: () {
// setState only rebuilds this one item.
setState(() {
_isFavorited = !_isFavorited;
});
},
),
);
}
}
Performance is not one big thing. It is a thousand small things. Building fast lists in Flutter boils down to a few core principles. Be lazy with ListView.builder
. Keep your build methods fast and pure. And use simple tools like const
to give the framework hints.
Get these small things right and your app will feel fast and fluid. Get them wrong and your users will feel it on every scroll.
— Rishi Banerjee
September 2025