Flutter 列表页卡顿,问题往往不在渲染,在重建

Flutter 列表页卡顿,问题往往不在渲染,在重建

Flutter 列表页卡顿,刚入手的开发者第一反应往往是"Flutter 渲染太慢"或者"手机性能不够"。

实际上 Flutter 处理几千条列表数据绑绑有余。大多数卡顿问题,根本原因是页面在反复重建,而不是渲染吃不消。

一个典型的慢列表

页面大概长这样:

  • 聊天消息列表
  • 订单商品列表
  • 动态信息流

初版写法很朴素:

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ChatBubble(
      message: items[index],
      onTap: () => _handleTap(items[index]),
    );
  },
)

数据量上来之后开始卡。再看 ChatBubble

class ChatBubble extends StatelessWidget {
  final Message message;
  final VoidCallback onTap;

  const ChatBubble({
    super.key,
    required this.message,
    required this.onTap,
  });

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Container(
        padding: EdgeInsets.all(12),
        child: Row(
          children: [
            Avatar(url: message.avatar),
            SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(message.nickname),
                  SizedBox(height: 4),
                  Text(message.content),
                ],
              ),
            ),
            Text(message.time),
          ],
        ),
      ),
    );
  }
}

看起来也没问题,StatelessWidget,build 逻辑也不复杂。

但只要父页面有任何状态变化,整个列表就会跟着重建。

为什么卡

原因 1:父组件状态变化导致整页重建

比如页面顶部有个筛选按钮,选中状态用 setState 管理:

class ChatListPage extends StatefulWidget {
  
  State<ChatListPage> createState() => _ChatListPageState();
}

class _ChatListPageState extends State<ChatListPage> {
  String _filterType = 'all';

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        FilterChips(
          selected: _filterType,
          onChanged: (v) => setState(() => _filterType = v),
        ),
        Expanded(
          child: ListView.builder(
            itemCount: items.length,
            itemBuilder: (context, index) {
              return ChatBubble(
                message: items[index],
                onTap: () => _handleTap(items[index]),
              );
            },
          ),
        ),
      ],
    );
  }
}

每次切换筛选,_filterType 变化,触发 _ChatListPageState.build(),进而触发 ListView.builder

ListView.builder 虽然只渲染可见项,但每次 build 都会创建新的 ChatBubble widget 实例。Flutter 需要对比新旧 widget 树来判断是否需要更新。这个比对本身就有开销,如果列表项组件本身比较复杂,卡顿会非常明显。

原因 2:ItemBuilder 里直接调父组件方法

itemBuilder: (context, index) {
  return ChatBubble(
    message: items[index],
    onTap: () => _handleTap(items[index]), // 每次build都创建新闭包
  );
},

闭包会捕获 _ChatListPageState,只要父组件 rebuild,onTap 这个参数就变成一个新的匿名函数对象。虽然实际逻辑没变,但 Flutter 的 Widget.canUpdate 会认为这是个新 widget,触发更新。

原因 3:列表项包含频繁变化的状态

比如消息列表里有已读未读状态、发送中状态:

itemBuilder: (context, index) {
  return ChatBubble(
    message: items[index],
    isSending: _sendingIds.contains(items[index].id), // 列表外状态
    isRead: _readIds.contains(items[index].id),      // 列表外状态
  );
},

只要 _sendingIds_readIds 变化,即使列表数据本身没变,整个列表也会被迫重建。

先定位:到底是重建还是渲染慢

Flutter DevTools 的 Performance 视图能清晰展示这一点:

  • 如果 Frame Rendering 阶段很长,说明是渲染问题
  • 如果 Widget Rebuild 阶段很长,说明是重建问题

大多数场合下,你看到的卡顿是后者。

快速自检:把列表项 const 化,看卡顿是否有改善。

itemBuilder: (context, index) {
  return const ChatBubble(
    message: null, // 只是测试,实际不可行
  );
},

如果改 const 之后流畅度明显提升,说明根因在重建。

解决方式 1:用 const 构造减少重建

widget 的构造如果能走 const,Flutter 可以在重建时直接复用实例。

class ChatBubble extends StatelessWidget {
  final Message message;
  final VoidCallback onTap;

  const ChatBubble({
    super.key,
    required this.message,
    required this.onTap,
  });

只要 messageonTap 没变,Flutter 会认为这个 widget 和上次一样,跳过更新。

onTap 这个闭包每次 build 都是新创建的,所以上面的写法还不够。

解决方式 2:用 RepaintBoundary 隔离渲染区域

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return RepaintBoundary(
      child: ChatBubble(
        message: items[index],
        onTap: () => _handleTap(items[index]),
      ),
    );
  },
)

RepaintBoundary 的作用是把这个区域和父组件的重建隔离开。父组件 rebuild 时,只要 ChatBubble 自身不需要更新,Flutter 就不需要重绘这一块。

消息列表这类场景,RepaintBoundary 效果通常很明显。

解决方式 3:列表项状态提到列表外,用 itemExtent 优化

如果列表项高度固定,可以指定 itemExtent,省去 Flutter 计算高度的开销:

ListView.builder(
  itemCount: items.length,
  itemExtent: 72, // 每个列表项固定高度
  itemBuilder: (context, index) {
    return ChatBubble(
      message: items[index],
      onTap: () => _handleTap(items[index]),
    );
  },
)

这个优化对长列表效果显著,尤其在快速滚动时。

解决方式 4:用 GlobalKey 缓存列表项状态

对于聊天列表这种场景,消息状态(发送中、已发送、失败)需要在列表项内部管理,但父组件又需要能更新这些状态。

常见的做法是给列表项加 GlobalKey,通过 key 直接操作子组件状态:

class ChatBubbleState extends State<ChatBubble> {
  bool isSending = false;

  void setSending(bool sending) {
    setState(() => isSending = sending);
  }
}

class ChatListPageState extends State<ChatListPage> {
  final Map<String, GlobalKey<ChatBubbleState>> _itemKeys = {};

  GlobalKey<ChatBubbleState> _getKey(Message msg) {
    return _itemKeys.putIfAbsent(msg.id, () => GlobalKey());
  }

  void _onSendResult(String msgId, bool success) {
    final key = _itemKeys[msgId];
    if (key?.currentState != null) {
      key!.currentState!.setSending(!success);
    }
  }
}

这样列表项内部状态变化不会触发父组件重建。父组件只需要在发送结果返回时,通过 key 通知对应列表项更新。

解决方式 5:大列表考虑 ListView.customSliverList

如果列表数据量很大(超过几百条),标准 ListView.builder 仍然会有压力。可以考虑用 SliverList 配合 SliverChildBuilderDelegate,并设置 addAutomaticKeepAlives: falseaddRepaintBoundaries: false 来进一步减少开销:

return CustomScrollView(
  slivers: [
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => ChatBubble(
          message: items[index],
          onTap: () => _handleTap(items[index]),
        ),
        childCount: items.length,
        addAutomaticKeepAlives: false,
        addRepaintBoundaries: false,
      ),
    ),
  ],
)

但这要谨慎用。只有在列表项本身很轻量且没有状态依赖时才能这么做。

解决方式 6:分页数据用 ListView.builder 的正确姿势

加载更多数据时,不要直接往现有列表头部插入数据:

// 错误做法
items.insertAll(0, newMessages);
setState(() {});

这会导致整个列表重建。正确做法是管理两个列表:

List<Message> _newMessages = [];
List<Message> _oldMessages = [];

void loadMore() {
  _newMessages.addAll(newData);
  setState(() {
    _newMessages = List.from(_newMessages);
  });
}

或者用 AnimatedList 配合分页,让新数据的加入有明确的可动画边界。

最后总结

Flutter 列表卡顿,大多数时候是这几个问题叠加:

  1. 父组件状态变化导致整页 rebuild
  2. ItemBuilder 里创建了新的闭包或对象
  3. 列表项依赖了外部状态但没有隔离
  4. 列表项没有声明 const 构造
  5. 没有用 RepaintBoundary 隔离渲染区域

性能优化的顺序通常是:

  1. 先用 DevTools 确认是重建问题还是渲染问题
  2. RepaintBoundary,成本最低效果最明显
  3. const 构造和稳定 keys 减少无效重建
  4. 考虑 itemExtent 优化滚动性能
  5. 大列表考虑 Sliver 方案

把这些检查项过一遍,大多数 Flutter 列表卡顿问题都能找到根因并解决。

最后更新 4/19/2026, 11:52:19 PM