文章

Flutter 列表性能优化深度解析

Flutter 长列表优化:ListView.builder、Sliver、itemExtent、懒加载与缓存。 讲清各类列表方案取舍,掌握后能答出「万级数据列表怎么不卡」。

Flutter 列表性能优化深度解析

一句话概括

Flutter 列表能扛十亿行数据的秘密就三个字:懒构建。ListView.builder 只渲染屏幕上看得见的 Item,滚出去的直接销毁——不是 “隐藏”,是 “杀了”。再加上 itemExtent、ValueKey、分页加载三件套,列表性能基本到顶。

核心知识点

1. ListView.builder vs ListView(children):天壤之别

1
2
3
4
5
6
7
8
// ❌ 全量构建:10000 个 Item 全部创建 Widget + Element + RenderObject
ListView(children: List.generate(10000, (i) => Text('Item $i')))

// ✅ 懒构建:只创建屏幕上可见的 4-5 个
ListView.builder(
  itemCount: 10000,
  itemBuilder: (context, i) => Text('Item $i'),
)

全量构建的 ListView(children: []) 在 20 个 Item 以内还行,超过就是内存炸弹。builder 模式:Flutter 先算 “第 500 到第 505 个 Item 在屏幕上”,然后只调 6 次 builder。滚到第 499,第 505 被销毁、第 499 被创建——内存占用稳定。

2. itemExtent:让滚动条不发抖

1
2
3
4
5
ListView.builder(
  itemCount: 100000,
  itemExtent: 56.0,  // 每个 Item 固定高度
  itemBuilder: (_, i) => SizedBox(height: 56, child: Text('Item $i')),
)

设了 itemExtent,Flutter 直接 index * 56 = 滚动偏移量,不需要测量每个 Item 的实际高度。滚动总长度瞬间算出来,滚动条不再忽大忽小。代价是 Item 高度必须统一——聊天消息这种高度不固定的不能用。

3. ValueKey:Item 状态不丢失的保险

1
2
3
4
5
6
7
8
9
10
11
12
// 无 Key:按位置匹配。列表重排后,第 3 个 Item 拿到原来第 1 个的 Checkbox 状态
ListView.builder(
  itemBuilder: (_, i) => CheckboxTile(title: items[i].name),
)

// 有 ValueKey:按 ID 匹配。重排后,Widget 带着自己的 Checkbox 状态移动到新位置
ListView.builder(
  itemBuilder: (_, i) => CheckboxTile(
    key: ValueKey(items[i].id),
    title: items[i].name,
  ),
)

Key 的本质是告诉 Flutter 的 diff 引擎 “这两个 Item 是同一个东西,只是换了位置”。没有 Key 的时候,Flutter 只能靠 widget 类型和位置判断——位置一变就认为 “旧的删了,新的来了”,状态全丢。

4. 无限滚动分页:三段式标准模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class PagedList extends StatefulWidget { /* ... */ }

class _PagedListState extends State<PagedList> {
  final _items = <String>[];
  final _controller = ScrollController();
  bool _loading = false, _hasMore = true;

  @override
  void initState() {
    super.initState();
    _controller.addListener(() {
      if (_controller.position.pixels >= _controller.position.maxScrollExtent - 200) {
        _loadMore();
      }
    });
    _loadMore();
  }

  Future<void> _loadMore() async {
    if (_loading || !_hasMore) return;
    setState(() => _loading = true);
    final newItems = await fetchPage(_items.length, 20);
    setState(() {
      _items.addAll(newItems);
      _loading = false;
      _hasMore = newItems.length == 20;
    });
  }

  @override
  Widget build(BuildContext context) => ListView.builder(
    controller: _controller,
    itemCount: _items.length + (_hasMore ? 1 : 0),
    itemBuilder: (_, i) => i >= _items.length
      ? const Center(child: CircularProgressIndicator())
      : ListTile(title: Text(_items[i])),
  );

  @override
  void dispose() { _controller.dispose(); super.dispose(); }
}

三段式:ScrollController 监听 → maxScrollExtent - 200 触发 → setState 追加数据。200px 的阈值让你还没滚到底就已经在加载下一批了。

5. 图片列表专项优化

1
2
3
4
5
6
7
8
9
10
11
12
Image.network(url,
  width: double.infinity,
  height: 160,  // 固定高度,防止图片加载后撑开布局
  fit: BoxFit.cover,
  loadingBuilder: (_, child, progress) =>
    progress == null ? child : const SizedBox(height: 160, child: Center(child: CircularProgressIndicator())),
  errorBuilder: (_, __, ___) => Container(height: 160, color: Colors.grey[200], child: const Icon(Icons.broken_image)),
);

// 全局配置
PaintingBinding.instance.imageCache.maximumSize = 500;
PaintingBinding.instance.imageCache.maximumSizeBytes = 100 << 20;

固定高宽 + loadingBuilder 占位 = 杜绝 “布局抖动”。图片加载前后 Item 高度不变,列表不会跳。maximumSize 调大可以让更少的图片反复解码。

其实你每天都在用

  • 微信聊天列表 — 几千条消息,builder 只渲染屏幕上的 6-7 条,上面滚出去的直接回收
  • 淘宝商品瀑布流 — SliverGrid + builder,两列无限滚动,三个屏幕外的 Item 根本不存在
  • 通讯录快速定位 — ScrollController.jumpTo(index * itemExtent),设了固定高度连测量都不用
  • 朋友圈图片九宫格 — loadingBuilder 在快速滑动时先占位,停稳了解码才真正开始
  • 下拉刷新 + 加载更多 — RefreshIndicator + ScrollController 监听,标准的分页模板

常见误解(FAQ)

  • ❌ 误区:「ListView.builder 把所有 Item 都创建了,只是隐藏了」 builder 模式直接调 deactivateChild 把滚出可视区的 Element 从树里摘掉——不是隐藏,是销毁。你可以用 DevTools 看:Widget 树里永远只有 5-8 个 Item。

  • ❌ 误区:「cacheExtent 设得越大越好」 cacheExtent 是提前构建的缓冲区,设大了会让几十个 Item 同时 build → 首帧变慢。默认 250px 对大多数场景足够,除非 Item 里有大图需要提前解码。

  • ❌ 误区:「用 Column + SingleChildScrollView = ListView」 Column 里的所有子 Widget 全部构建。1000 个 Item 的 Column = 1000 次 build + 1000 个 RenderObject。SingleChildScrollView 只解决滚动不解决回收。

  • ❌ 误区:「addAutomaticKeepAlives: false 能显著提升性能」 Flutter 默认给每个列表 Item 加了 KeepAlive 通知和 RepaintBoundary。关掉它们可能省几个字节,但你的 TabBarView 切回来时 Item 状态全丢——得不偿失。

一句话总结

列表性能 = 懒构建 + 固定高 + ID Key + 分页加载。四个关键词记住了,一百行数据和一亿行数据的体验差距,基本为零。

本文由作者按照 CC BY 4.0 进行授权