Flutter 列表滑动卡顿通常不是单一原因,更常见的是「每帧构建成本 + 布局约束 + 图片解码 + 重建范围」叠在一起。建议先判断卡在哪个线程,再动手改:UI 线程每帧 build/layout 超时,就先减构建和布局;Raster 线程超时,就先查图片解码、阴影、裁剪和半透明合成。目标不要定成「完全不掉帧」,而是定成「在目标机型的 profile 或 release 包上,常见滚动操作不出现连续可感知的停顿」。
适用场景:列表在真机 profile/release 包下滚动出现明显掉帧、白块或响应延迟,且问题可重复出现。操作动作:先用 DevTools 的 frame chart 分清是 UI 线程还是 Raster 线程超时,再按「对齐列表结构 → 降低每帧构建量 → 收敛重建范围 → 处理图片与绘制特效」的顺序改,不要一次改多处。验证方式:同一机型、同一滚动动作,改动前后各录一次 timeline 对比,看最长帧和连续卡顿段。风险边界:debug 模式与模拟器上的卡顿不能作为优化依据;itemExtent、RepaintBoundary 这类改动有前提条件,用错会引入错位或内存问题。
先确认当前场景和目标
动手前先把「卡」拆成可描述的现象,否则改完也无法判断是否变好。可以先确认下面几项:
- 运行模式:用
flutter run起的是 debug 包,走 JIT,滚动卡顿很常见,不能当作问题证据。需要在真机上用flutter run --profile或 release 包复现。 - 卡顿形态:是进入页面首次滑动就卡,还是快速甩动才卡;是滚动中图片渐显时卡,还是点了某个按钮之后卡。图片渐显卡偏 Raster,点击后卡偏重建范围,首次进入卡偏布局与首帧构建。
- 列表规模与项结构:项数、每项是否含网络图、是否圆角裁剪与阴影、是否在列表项里再嵌一个可滚动容器。
- 目标机型:低端安卓机和中高端机的上限差很多,建议明确「在哪台设备上达到什么程度算可接受」。
这几项确认完,再决定优化范围。若目标机是低端安卓机,通常要优先砍每项的视觉复杂度,而不是先做微优化。
按这个顺序处理
第一步,先对齐列表结构。能用 ListView.builder 或 SliverList 就不要用 Column 加 SingleChildScrollView 铺全部项,后者会把所有项一次性建出来。项高固定时用 itemExtent,可以让滚动位置计算跳过逐项测量:
ListView.builder(
itemCount: items.length,
itemExtent: 72, // 仅当每项高度确实固定
itemBuilder: (context, index) => RowItem(item: items[index]),
)
同时检查有没有外层 ListView 套内层 shrinkWrap: true 加 NeverScrollableScrollPhysics 的写法,这种嵌套会让内层列表在每帧重复测量。

第二步,降低每帧构建量。把列表项提成独立的 StatelessWidget,能加 const 的构造加 const。更要紧的是别在 itemBuilder 里做重活:排序、JSON 解析、长文本拼接、日期格式化、正则匹配,这些都建议挪到数据层预计算,列表只负责展示。
第三步,收敛重建范围。整页 setState 会带着可见项一起重建。把会变化的值下沉到 ValueListenableBuilder、AnimatedBuilder 这类局部构建器,静态子树通过 child 参数传进去:
ValueListenableBuilder<int>(
valueListenable: counter,
builder: (context, value, child) => Row(
children: [child!, Text('$value')],
),
child: const ProductTitle(),
)
第四步,处理图片与绘制特效。网络图给固定宽高,并按逻辑尺寸乘设备像素比给解码尺寸,避免原图整张解码进内存:

final dpr = MediaQuery.of(context).devicePixelRatio;
Image.network(
url,
width: 56,
height: 56,
fit: BoxFit.cover,
cacheWidth: (56 * dpr).round(),
)
列表项里大面积 ClipRRect、BackdropFilter、ShaderMask、多层 Opacity 都会抬高绘制成本,能换成静态圆角图或直接切图的先换。确有个别项重绘频繁时,再对该项加 RepaintBoundary。
第五步,回头看 timeline。上面改完还卡,就用 DevTools 的 Performance 页逐帧看 build、layout、paint 哪段最长,以及是否频繁 GC。这一步是定位,不是继续堆技巧。
改完后怎么验证
验证要和定位用同一套环境,否则结论不成立。建议固定一台目标真机、固定几个动作:慢速滑动、快速甩动、滑到底再回顶部、滚动中触发图片加载。
- 用
flutter run --profile起包,打开 DevTools 的 Performance,分别录制改动前后的同一组动作。 - 看 frame chart 里 UI 与 Raster 两条时间线,重点看最长帧和是否出现连续超时段,不要只看平均帧时间。
- 观察重建次数:在列表项的 build 里临时计数打印,确认滚动时是否还在整页重建。打印本身会影响性能,只用于定位,确认后删掉。
- 在 DevTools 的 Memory 里滚动多轮,确认图片缓存有没有持续增长。
预期信号是:常速滑动时不再出现连续可感知的停顿,快速甩动时最长帧明显收敛,图片渐显阶段不再阻塞 Raster 线程。如果某一项改动没有带来可见变化,建议先回退再继续定位,避免多个改动叠在一起无法归因。
容易忽略的边界
itemExtent和prototypeItem只在项高固定时适用。列表项存在展开收起、字号随系统无障碍设置放大时,写死高度会截断内容或错位,需要改用自适应测量。RepaintBoundary不是越多越好,每项都加会抬高内存与合成成本,只对确有独立重绘内容的项加。cacheWidth写死像素值,在分屏、折叠屏、横竖屏切换时会变糊或浪费内存,建议按运行时逻辑尺寸乘像素比计算。shrinkWrap有时是设计需要,比如列表嵌在整页滚动里且高度有限。改成 Sliver 结构要动布局,先评估重构成本再决定。- 第三方组件自身可能有滚动开销,例如下拉刷新、轮播、复杂卡片。改业务代码前先确认开销来源,避免优化错地方。
- debug 模式、模拟器、热重载后的状态都不能作为性能结论。若改动后真机仍有轻微掉帧,优先简化列表项的视觉元素,而不是继续叠微优化。