Optimizing list performance in Flutter
By Ann Tech · 31 January 2025
Long lists, image-heavy feeds, and deeply nested cards are common sources of Flutter performance problems. This guide covers the specific optimizations that make lists smooth even at 120 FPS.
Use ListView.builder, not ListView with children
The most impactful change for any list:
// BAD: builds all 1000 items even if only 10 are visible
ListView(
children: orders.map((o) => OrderCard(order: o)).toList(),
)
// GOOD: only builds visible items + a few extra (itemExtent helps more)
ListView.builder(
itemCount: orders.length,
itemBuilder: (_, i) => OrderCard(order: orders[i]),
)
Provide itemExtent for uniform-height lists
When all items have the same height, itemExtent lets Flutter skip layout calculations entirely:
ListView.builder(
itemCount: orders.length,
itemExtent: 80.0, // Each item is exactly 80px tall
itemBuilder: (_, i) => OrderCard(order: orders[i]),
)
For variable-height lists where you know sizes in advance, use SliverVariedExtentList.
Cache images
Loading network images causes layout jumps and network requests on every scroll. Use CachedNetworkImage:
CachedNetworkImage(
imageUrl: order.imageUrl,
// Pre-calculate the exact size to avoid layout shifts
width: 80,
height: 80,
fit: BoxFit.cover,
memCacheWidth: 160, // 2x for retina, no larger
placeholder: (_, __) => const ShimmerBox(width: 80, height: 80),
errorWidget: (_, __, ___) => const Icon(Icons.broken_image),
)
memCacheWidth and memCacheHeight are crucial — without them, a 1200x800 product image is decoded at full resolution and cached in memory. For 100 list items, that's hundreds of MB.
Avoid rebuilding list items
List items rebuild when the parent rebuilds. Prevent this:
// BAD: every property access in the builder creates a new object
ListView.builder(
itemBuilder: (_, i) {
return Container(
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)), // New object every scroll
child: OrderCard(order: orders[i]),
);
},
)
// GOOD: use const or extract to a stateless widget
class OrderListItem extends StatelessWidget {
const OrderListItem({super.key, required this.order});
final Order order;
static const _decoration = BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(8)),
);
@override
Widget build(BuildContext context) {
return Container(
decoration: _decoration,
child: OrderCard(order: order),
);
}
}
Keys for animated lists
When items can be reordered or filtered, keys tell Flutter which widget maps to which data:
ListView.builder(
itemCount: orders.length,
itemBuilder: (_, i) => OrderCard(
key: ValueKey(orders[i].id), // Stable key
order: orders[i],
),
)
Without keys, Flutter reuses the widget at position 0 for the new first item — which causes visual glitches for stateful widgets.
Deferred image loading
For long lists, pre-load images ahead of the scroll position:
class OrderCard extends StatefulWidget {
const OrderCard({super.key, required this.order});
final Order order;
@override
State<OrderCard> createState() => _OrderCardState();
}
class _OrderCardState extends State<OrderCard> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Pre-cache image
if (widget.order.imageUrl.isNotEmpty) {
precacheImage(NetworkImage(widget.order.imageUrl), context);
}
}
// ...
}
Sliver lists for mixed content
For screens that mix a header, a list, and a footer (a common pattern), use Slivers instead of ListView nested inside SingleChildScrollView (which disables virtualization):
CustomScrollView(
slivers: [
const SliverAppBar(title: Text('Orders'), floating: true),
SliverToBoxAdapter(
child: FilterBar(onFilterChanged: _onFilter),
),
SliverList.builder(
itemCount: orders.length,
itemBuilder: (_, i) => OrderCard(order: orders[i]),
),
if (isLoadingMore)
const SliverToBoxAdapter(
child: Center(child: CircularProgressIndicator()),
),
],
)
Pinning expensive computations
Don't do computation in itemBuilder — it runs on every scroll frame:
// BAD: formats date on every build
itemBuilder: (_, i) => OrderCard(
dateLabel: DateFormat.yMMMd().format(orders[i].createdAt), // Called every frame
order: orders[i],
)
// GOOD: pre-compute before building the list
final formattedDates = orders.map((o) =>
DateFormat.yMMMd().format(o.createdAt)
).toList();
itemBuilder: (_, i) => OrderCard(
dateLabel: formattedDates[i], // Already computed
order: orders[i],
)
Profile with DevTools
Before optimizing, measure:
flutter run --profile
In DevTools → Performance:
- Record scrolling
- Look for red frames (> 16ms)
- Inspect the flame chart for the slow frame
- Look for
build,layout, andpainttaking > 2ms each
Common pitfalls
Using SingleChildScrollView + Column for long lists. This builds everything and has no virtualization. Switch to ListView.builder.
Large images without memCacheWidth. Decoded full-resolution images eat memory. Always set memCacheWidth/memCacheHeight on CachedNetworkImage.
Not using const for list item sub-widgets. List item sub-widgets that could be const but aren't will rebuild on every scroll event. The prefer_const_constructors lint catches most of these.
Sign in to like, dislike, or report.