Lazy List Building with ListView.builder and Slivers
Lazy List Building with ListView.builder and Slivers
One of the most impactful performance optimisations in Flutter is lazy rendering — building only the widgets that are currently visible on screen, rather than constructing every item in a collection up front. When a list contains hundreds or thousands of items, eagerly building all of them during the first frame wastes memory and CPU, causing janky animations and slow start-up. ListView.builder, GridView.builder, and the Sliver family solve this problem elegantly.
Why Eager Construction Hurts Performance
The standard ListView constructor accepts a children list. Every widget in that list is instantiated and laid out before the first frame is drawn — even items that are far below the fold and will never be seen without scrolling. For small, fixed-size lists this is fine, but for dynamic or large collections it is a serious bottleneck:
- All item widgets are created and kept in memory simultaneously.
- Layout is computed for every item, regardless of visibility.
- Adding items triggers a full rebuild of the
childrenlist. - The app jank increases proportionally with list length.
List<Widget> built with .map(...).toList() to ListView(children: ...) for lists with more than ~20–30 items. Use ListView.builder instead.ListView.builder — On-Demand Widget Construction
ListView.builder accepts an itemBuilder callback and an optional itemCount. It calls the callback only for indices that intersect the current viewport, plus a small configurable cache extent. Items scrolled completely off-screen are disposed and their memory is reclaimed automatically.
ListView.builder — Basic Usage
class ContactList extends StatelessWidget {
final List<Contact> contacts;
const ContactList({super.key, required this.contacts});
@override
Widget build(BuildContext context) {
return ListView.builder(
// itemCount prevents the builder from running past the end of the list
itemCount: contacts.length,
// itemBuilder is called lazily — only for visible indices
itemBuilder: (BuildContext context, int index) {
final contact = contacts[index];
return ListTile(
leading: CircleAvatar(child: Text(contact.initials)),
title: Text(contact.name),
subtitle: Text(contact.phone),
);
},
);
}
}
Key parameters that control laziness and performance:
itemCount— Tells the scroll controller the exact extent; omit only for infinite / unknown-length lists.cacheExtent— Pixels beyond the viewport to pre-build (default 250 px). Increase for smoother flings; decrease to save memory.addRepaintBoundaries— Wraps each item in aRepaintBoundaryby default, isolating repaints to individual tiles.addAutomaticKeepAlives— Keeps items alive if they contain aKeepAliveClientMixinwidget.
GridView.builder — Lazy 2-D Grids
GridView.builder applies the same deferred-construction strategy to grids. It accepts a SliverGridDelegate that controls column count and spacing, and an itemBuilder that is only invoked for cells in the visible area.
GridView.builder — Photo Grid
class PhotoGrid extends StatelessWidget {
final List<String> imageUrls;
const PhotoGrid({super.key, required this.imageUrls});
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
),
itemCount: imageUrls.length,
itemBuilder: (context, index) {
return Image.network(
imageUrls[index],
fit: BoxFit.cover,
// Fade-in placeholder while the image loads
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(child: CircularProgressIndicator());
},
);
},
);
}
}
Sliver Widgets — Fine-Grained Lazy Layout
Slivers are Flutter's low-level scrollable primitives. Every ListView and GridView is actually a thin wrapper around a Sliver underneath. Using Slivers directly via CustomScrollView lets you compose complex scroll experiences — sticky headers, collapsing app bars, mixed list-and-grid sections — all sharing a single scroll controller and a single lazy rendering budget.
SliverListwith aSliverChildBuilderDelegate— lazy equivalent ofListView.builder.SliverGridwith a builder delegate — lazy 2-D grid inside a custom scroll view.SliverAppBar— collapsing or pinned header that participates in the same scroll physics.SliverToBoxAdapter— wraps any fixed-size widget (e.g., a banner) inside aCustomScrollView.SliverFixedExtentList— even faster thanSliverListwhen all items have the same height, because layout skips the height-measurement step entirely.
CustomScrollView with Mixed Slivers
class FeedPage extends StatelessWidget {
final List<String> stories;
final List<Post> posts;
const FeedPage({super.key, required this.stories, required this.posts});
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
// Collapsing app bar — part of the scroll budget
const SliverAppBar(
title: Text('Feed'),
floating: true,
snap: true,
expandedHeight: 120,
),
// Fixed-height story strip (non-lazy, small count)
SliverToBoxAdapter(
child: SizedBox(
height: 80,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: stories.length,
itemBuilder: (_, i) => StoryAvatar(url: stories[i]),
),
),
),
// Lazy post list — only visible items are built
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => PostCard(post: posts[index]),
childCount: posts.length,
),
),
],
);
}
}
SliverList have an identical, known height, switch to SliverFixedExtentList and set itemExtent. Flutter can then jump directly to any scroll offset without walking the entire list — a significant win for lists with thousands of items.Keys and Item Identity in Lazy Lists
Because items are created and destroyed as the user scrolls, Flutter must be able to reconcile the new widget tree with the old one efficiently. Providing a stable key derived from your data (e.g., ValueKey(contact.id)) prevents unnecessary rebuilds when the list reorders and ensures that stateful children (animations, text fields) retain their state across scroll events.
ListView.builder or GridView.builder for any collection with more than a handful of items. Reach for CustomScrollView with Sliver delegates when you need to compose multiple scrollable sections, sticky headers, or collapsing app bars. Assign stable ValueKeys to stateful list items to preserve state across scrolling. Together, these techniques ensure your scrollable UIs remain smooth and memory-efficient at any scale.