返回文章列表

iOS 性能优化 系列 03|列表为什么会卡?UITableView、CollectionView、SwiftUI List 的共性问题

列表卡顿通常不是某个控件“天生不行”,而是滚动过程中主线程承担了太多与当前帧竞争的工作

列表卡顿是 iOS 项目里最常见、也最容易被误判的一类性能问题。

很多人一看到列表不顺,就会立刻说:

  • 是不是 SwiftUI List 太慢
  • 是不是 UICollectionView 配置不对
  • 是不是这个控件天生就卡

这些判断有时并不是完全错,但它们通常只是表层。
真正更核心的问题通常不是“控件是谁”,而是:

在滚动这件极度依赖主线程及时响应的动作里,你到底让系统同时做了多少额外工作。

这也是为什么 UITableViewUICollectionViewSwiftUI List 虽然实现方式不同,却经常会栽在非常相似的问题上。

一、列表为什么比很多页面更容易暴露性能问题

因为列表是一个高频、连续、对每一帧都敏感的场景。

用户在滚动时,系统需要不断完成这些事情:

  • 计算布局
  • 准备可见单元
  • 复用和回收单元
  • 绘制内容
  • 响应手势

如果这时你还让主线程同时承担很多额外工作,比如:

  • 图片解码
  • 富文本拼装
  • JSON 解析
  • 高成本 Auto Layout 计算
  • 频繁状态变更导致全量刷新

那卡顿几乎就是必然结果。

列表之所以高发,不是因为它神秘,而是因为它把主线程压力放大得特别明显。

二、最常见的问题,不是单元本身复杂,而是滚动时做了不该在这一刻做的事

很多人优化列表时,只盯着 cell 代码长度。
但真实项目里,列表卡的原因往往更像下面这些:

  • 图片在显示前才开始解码
  • 文本高度在滚动过程中反复计算
  • cell 每次出现都重新做格式化和数据转换
  • 列表滚动时触发了太多父层状态更新
  • 一次小变动导致整个列表重绘或 diff 成本过高

这些问题共同特点是:
它们本来不是“不能做”,而是“不该在滚动这个时刻做”。

所以列表优化最重要的思路不是“让每个 cell 变得绝对简单”,而是:

  • 哪些工作能提前
  • 哪些工作能缓存
  • 哪些工作能延后
  • 哪些更新可以局部化

三、图片往往是列表卡顿的高频元凶,不是因为下载,而是因为解码和尺寸处理

很多团队一遇到列表卡,就说“图片太多了”。
这句话方向有时是对的,但原因往往讲错了。

真正拖慢滚动的,很多时候不是网络下载本身,而是:

  • 图片解码
  • 图片缩放
  • 不合适的尺寸加载
  • 频繁触发主线程上的图像处理

也就是说,图片问题最可怕的不是“资源大”,而是它很容易在滚动的关键路径上插进高成本工作。

所以列表里的图片优化,核心通常不只是缓存,而是:

  • 提前准备合适尺寸
  • 减少显示瞬间的解码压力
  • 避免滚动时做过重图像处理

四、数据处理也经常会悄悄拖垮滚动体验

这点比很多人想的更常见。

例如一个列表项需要展示:

  • 时间格式化
  • 金额格式化
  • 富文本组合
  • 标签映射
  • 复杂状态文案

如果这些事情都在 cell 配置阶段临时做,滚动时主线程就会持续吃这笔成本。

这类问题最麻烦的地方在于:

  • 功能上完全正确
  • 单次开销看起来也不夸张
  • 但在高频滚动里,它会被成倍放大

所以列表性能优化很重要的一条原则是:
让展示层尽量消费已经准备好的展示数据,而不是边滚边做大量转换。

五、状态更新方式不对,列表也会卡,而且经常被误认成布局问题

有些列表问题根本不是 cell 复杂,也不是图片太大,而是状态更新粒度太粗。

例如:

  • 搜索关键词一变化就刷新整个列表
  • 点赞一个 item,整个页面状态树都重建
  • 一个分页结果回来,整个列表重新计算

这类问题非常容易被误诊为:

  • UI 组件太慢
  • 布局系统效率不高

实际上真正的问题往往是:

一次很小的业务变化,引发了远大于必要范围的 UI 更新。

所以列表卡顿排查时,我很常问的一句话是:

这次状态变化,到底应该影响哪几个 item,为什么最后影响了这么大一片区域?

六、UITableViewUICollectionViewSwiftUI List 的共性问题到底是什么

虽然这三个体系在实现细节上差异很大,但高频性能问题其实很像:

  • 滚动过程中做了太多主线程工作
  • 列表项显示数据准备太晚
  • 图片和文本处理放在了关键路径
  • 刷新粒度过大
  • 某些布局或视图层级过于复杂

也就是说,它们的共性问题并不是“同一个 API 缺陷”,而是:

列表这个交互形态本身,对主线程时机和工作分配特别敏感。

这也是为什么同样的错误设计,可以在三种不同列表控件里都复现出相似卡顿。

七、一个更接近实战的排查顺序

如果我今天要查一个列表为什么会卡,我通常不会先改代码,而会先按这个顺序判断:

  1. 卡顿发生在首屏出现、快速滚动,还是分页时。
  2. 当前列表项是否有图片、富文本或复杂布局。
  3. cell 展示前是否做了很多即时转换。
  4. 某次局部状态变化是否触发了过大范围刷新。
  5. 是否存在滚动过程中同步解码、缩放或重计算。

这个顺序的价值是:
先把“什么工作在和滚动抢主线程”找出来,而不是先猜框架问题。

八、结论:列表卡顿的本质,通常是关键路径上塞了太多不该塞的工作

如果只用一句话总结,我会说:

列表为什么会卡,核心通常不是某个控件本身不行,而是滚动这个高频场景里,主线程同时背了太多布局、解码、转换和过度刷新的成本。

所以列表优化最重要的不是“换控件”,而是:

  • 把工作提前
  • 把结果缓存
  • 把刷新粒度缩小
  • 把关键路径上的额外负担拿掉

这四件事做对了,列表体验通常会比单纯调某几个参数更稳定。