← Articles

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:

  1. Record scrolling
  2. Look for red frames (> 16ms)
  3. Inspect the flame chart for the slow frame
  4. Look for build, layout, and paint taking > 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.

Optimizing list performance in Flutter — ANN Tech