iOS 性能优化 系列 03|列表为什么会卡?UITableView、CollectionView、SwiftUI List 的共性问题
列表卡顿通常不是某个控件“天生不行”,而是滚动过程中主线程承担了太多与当前帧竞争的工作
列表卡顿是 iOS 项目里最常见、也最容易被误判的一类性能问题。
很多人一看到列表不顺,就会立刻说:
- 是不是
SwiftUI List太慢 - 是不是
UICollectionView配置不对 - 是不是这个控件天生就卡
这些判断有时并不是完全错,但它们通常只是表层。
真正更核心的问题通常不是“控件是谁”,而是:
在滚动这件极度依赖主线程及时响应的动作里,你到底让系统同时做了多少额外工作。
这也是为什么 UITableView、UICollectionView、SwiftUI List 虽然实现方式不同,却经常会栽在非常相似的问题上。
一、列表为什么比很多页面更容易暴露性能问题
因为列表是一个高频、连续、对每一帧都敏感的场景。
用户在滚动时,系统需要不断完成这些事情:
- 计算布局
- 准备可见单元
- 复用和回收单元
- 绘制内容
- 响应手势
如果这时你还让主线程同时承担很多额外工作,比如:
- 图片解码
- 富文本拼装
- JSON 解析
- 高成本 Auto Layout 计算
- 频繁状态变更导致全量刷新
那卡顿几乎就是必然结果。
列表之所以高发,不是因为它神秘,而是因为它把主线程压力放大得特别明显。
二、最常见的问题,不是单元本身复杂,而是滚动时做了不该在这一刻做的事
很多人优化列表时,只盯着 cell 代码长度。
但真实项目里,列表卡的原因往往更像下面这些:
- 图片在显示前才开始解码
- 文本高度在滚动过程中反复计算
- cell 每次出现都重新做格式化和数据转换
- 列表滚动时触发了太多父层状态更新
- 一次小变动导致整个列表重绘或 diff 成本过高
这些问题共同特点是:
它们本来不是“不能做”,而是“不该在滚动这个时刻做”。
所以列表优化最重要的思路不是“让每个 cell 变得绝对简单”,而是:
- 哪些工作能提前
- 哪些工作能缓存
- 哪些工作能延后
- 哪些更新可以局部化
三、图片往往是列表卡顿的高频元凶,不是因为下载,而是因为解码和尺寸处理
很多团队一遇到列表卡,就说“图片太多了”。
这句话方向有时是对的,但原因往往讲错了。
真正拖慢滚动的,很多时候不是网络下载本身,而是:
- 图片解码
- 图片缩放
- 不合适的尺寸加载
- 频繁触发主线程上的图像处理
也就是说,图片问题最可怕的不是“资源大”,而是它很容易在滚动的关键路径上插进高成本工作。
所以列表里的图片优化,核心通常不只是缓存,而是:
- 提前准备合适尺寸
- 减少显示瞬间的解码压力
- 避免滚动时做过重图像处理
四、数据处理也经常会悄悄拖垮滚动体验
这点比很多人想的更常见。
例如一个列表项需要展示:
- 时间格式化
- 金额格式化
- 富文本组合
- 标签映射
- 复杂状态文案
如果这些事情都在 cell 配置阶段临时做,滚动时主线程就会持续吃这笔成本。
这类问题最麻烦的地方在于:
- 功能上完全正确
- 单次开销看起来也不夸张
- 但在高频滚动里,它会被成倍放大
所以列表性能优化很重要的一条原则是:
让展示层尽量消费已经准备好的展示数据,而不是边滚边做大量转换。
五、状态更新方式不对,列表也会卡,而且经常被误认成布局问题
有些列表问题根本不是 cell 复杂,也不是图片太大,而是状态更新粒度太粗。
例如:
- 搜索关键词一变化就刷新整个列表
- 点赞一个 item,整个页面状态树都重建
- 一个分页结果回来,整个列表重新计算
这类问题非常容易被误诊为:
- UI 组件太慢
- 布局系统效率不高
实际上真正的问题往往是:
一次很小的业务变化,引发了远大于必要范围的 UI 更新。
所以列表卡顿排查时,我很常问的一句话是:
这次状态变化,到底应该影响哪几个 item,为什么最后影响了这么大一片区域?
六、UITableView、UICollectionView、SwiftUI List 的共性问题到底是什么
虽然这三个体系在实现细节上差异很大,但高频性能问题其实很像:
- 滚动过程中做了太多主线程工作
- 列表项显示数据准备太晚
- 图片和文本处理放在了关键路径
- 刷新粒度过大
- 某些布局或视图层级过于复杂
也就是说,它们的共性问题并不是“同一个 API 缺陷”,而是:
列表这个交互形态本身,对主线程时机和工作分配特别敏感。
这也是为什么同样的错误设计,可以在三种不同列表控件里都复现出相似卡顿。
七、一个更接近实战的排查顺序
如果我今天要查一个列表为什么会卡,我通常不会先改代码,而会先按这个顺序判断:
- 卡顿发生在首屏出现、快速滚动,还是分页时。
- 当前列表项是否有图片、富文本或复杂布局。
- cell 展示前是否做了很多即时转换。
- 某次局部状态变化是否触发了过大范围刷新。
- 是否存在滚动过程中同步解码、缩放或重计算。
这个顺序的价值是:
先把“什么工作在和滚动抢主线程”找出来,而不是先猜框架问题。
八、结论:列表卡顿的本质,通常是关键路径上塞了太多不该塞的工作
如果只用一句话总结,我会说:
列表为什么会卡,核心通常不是某个控件本身不行,而是滚动这个高频场景里,主线程同时背了太多布局、解码、转换和过度刷新的成本。
所以列表优化最重要的不是“换控件”,而是:
- 把工作提前
- 把结果缓存
- 把刷新粒度缩小
- 把关键路径上的额外负担拿掉
这四件事做对了,列表体验通常会比单纯调某几个参数更稳定。