Flutter 性能优化最佳实践
Flutter 性能优化最容易走偏的地方,通常有两个:
- 还没测量,就开始优化
- 只有用户反馈卡了,团队才想起来看性能
这两种都很常见,也都可以避免。
对 Flutter 3.41.6 和 Dart 3.11.4 来说,底层原则并没有变:
流畅不是靠“主观猜测优化”得来的,而是靠几个很朴素的事情——守住帧预算、控制 rebuild 范围、减少无谓的 layout / paint 开销、在真实设备上测,而不是拿模拟器自我安慰。
这篇文章只谈生产里真有用的做法。
先用 profile mode,看数据,不看感觉
性能问题不要在 debug mode 下判断。
更不要在模拟器里滑两下觉得“还行”。
真正要查运行时性能,建议从这几个入口开始:
- profile mode:用于分析问题
- 真机 release mode:用于最终确认
- DevTools:看 timeline、memory、rebuild、layout 线索
第一步要回答的问题,不是“怎么优化”,而是:
- 卡顿发生在 build?
- layout?
- paint?
- 图片解码?
- shader 编译?
- UI isolate 上跑了太重的异步任务?
- 启动阶段初始化太多?
- 包体和资源太重?
这一步不明确,后面的建议大多只是噪音。
脑子里要一直有“帧预算”这件事
60Hz 屏幕,一帧大约 16.67ms。
120Hz 屏幕,预算大概直接砍半。
这不是概念题,是你界面到底顺不顺手的硬约束。
实战里可以这么理解:
- 如果某个页面在你的开发机上已经贴着预算跑,放到中低端机上大概率就会掉帧
- 如果动画只在旗舰机上看起来顺,那不叫“性能没问题”
- 如果列表本来还行,一加载图片就开始抖,那问题就是图片链路,不要把它当成“另一个问题”
你要优化的不是 benchmark 分数,而是用户能不能明显感觉到掉帧。
先控制 rebuild 范围,再谈细枝末节
Flutter 里最常见的性能问题,不是什么底层渲染黑魔法,而是rebuild 范围太大。
常见表现包括:
- 一点小状态变化,整个页面都重建
- 平时滚动没问题,一旦父组件更新就开始掉帧
- 动画本身不复杂,但旁边不相关区域跟着一起重建
- Dashboard 这种页面功能一多就越来越脆
解决思路通常不是“找一个神奇 widget”,而是老老实实拆结构:
- 状态尽量放到真正使用它的地方
- 组件拆分按“变化频率”来拆,不只是按视觉区域拆
- 静态子树尽量保持稳定
- 该用
const的地方就用 - 不要在
build()里顺手做计算
一个很常见的坏模式是:
- 顶层页面状态管得太多
setState()打在树的高处- 任意一点交互都会让整页重建
更好的模式是:
- 把交互局部化
- 稳定的 child 尽量稳定传下去
- 重的区域彼此解耦
- 不相关状态不要绑在一起
一句话就是:
经常重建的组件必须足够便宜;不便宜的组件就不要让它经常重建。
build() 是热路径,不是模板文件
很多人写 build() 的心态,像在写模板。
但对复杂页面来说,build() 是热路径代码。
不适合放在 build() 里的东西包括:
- 重的解析逻辑
- 大列表排序、过滤
- 同步 JSON 处理
- 可以安全复用却反复创建的对象
- 每次都重新算一遍的派生值
如果某段转换逻辑是确定性的,而且会反复用到,就应该提前算,或者在合适的层级缓存,而不是每次 build 都顺手来一遍。
这不是说“什么都缓存”,而是说:
别把可以避免的工作,放在最频繁执行的 UI 路径里。
列表和网格,一开始就要按“高频场景”设计
真实 Flutter 应用里,性能问题最容易先在可滚动页面暴露出来。
一些基础规则大家都知道,但还是经常被忽略:
- 大集合优先 builder 构造
- 分页、流式加载,不要一次性全塞
- 不要 eager build 屏幕外子项
- item widget 尽量浅、尽量稳定
- 尽量给框架明确的尺寸信息
长列表最起码得先做到 lazy build:
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return MessageRow(item: items[index]);
},
)但 builder 只是起点,不是终点。
真正会拖慢列表的,往往是每一行自己太重,比如:
- layout 层级太深
- 明明只是小缩略图,却解码了大图
- 每个 item 都有复杂状态管理
- 可见区域里每行都在跑动画
- 时间格式化、金额格式化之类的逻辑每次都重算
一个真正顺的列表,结构通常都比较朴素。
这不是缺点,是成熟。
layout 成本,经常是看不见的税
页面看起来不复杂,不代表 layout 就便宜。
常见的 layout 成本来源有:
- 很深的
Row/Column/Intrinsic*嵌套 - 约束不清导致框架反复推导
- grid 想动态猜尺寸
- 明明尺寸规律固定,却每次都重新量内容
IntrinsicHeight、IntrinsicWidth 这类组件不是不能用,但一旦出现在性能敏感路径里,就应该提高警惕。
因为这往往意味着框架要先问一圈“你想多大”,再决定下一步怎么摆。
实战里比较靠谱的原则是:
- item 高度能定死就定死
- 卡片布局能标准化就标准化
- grid 如果能靠比例定尺寸,就别让它动态测
- 你已经知道的结构,不要让框架重新“发现”一遍
做性能排查时,DevTools 里的 layout 线索,很多时候比盯着 widget 树发呆更有帮助。
图片是最容易同时浪费内存和帧时间的地方
很多 Flutter 页面卡,不是 widget 的锅,是图片链路的锅。
典型问题包括:
- 小图位里塞超大原图
- 解码分辨率远大于实际显示尺寸
- 高速滚动列表里同时出现太多大图
- placeholder、缓存、解码尺寸都没认真管
- 预加载做得很猛,但没考虑内存
一些很实用的原则:
1. 能从服务端控制尺寸,就别全量下原图
Feed 缩略图、头像、小卡片封面,本来就不该下载大原图。
2. 解码尺寸尽量贴近显示尺寸
特别是列表项和头像这类高频图片。
不然下载是一笔,解码又是一笔,内存还是一笔。
3. 图片多的列表,本质上也是内存问题
很多“滚一会儿就开始抖”的页面,表面看像渲染问题,实际上是内存压力导致 GC 更频繁。
4. placeholder 要便宜
不要为了“加载中更精致”,再塞一个很复杂的小 UI。
这类页面里,占位图本来就应该足够轻。
5. 一定要在真机上看图片行为
尤其是内存小一点的 Android 设备,很快就会告诉你你是不是在浪费资源。
如果一个 feed 页面用久了越来越卡,先怀疑图片和内存,不要第一反应就是“Flutter 不行”。
异步不等于不会卡 UI
一个很常见的误解是:
“这是 async 的,所以不会影响界面。”
不对。
如果重活还是跑在主 isolate 上,用户一样会感受到卡顿。
常见问题包括:
- 大 JSON 解析
- 本地大规模过滤、聚合
- 压缩、加密
- 展示前对大集合做很重的 map / transform
- 启动阶段串行做太多初始化
如果工作量足够大,就应该考虑移出 UI isolate。
简单场景可以用 compute(),更复杂的可以考虑更明确的 isolate 设计。
但也不要把 isolate 当成银弹。
跨 isolate 通信本身也有成本。关键问题始终是:
主 isolate 是否因为这些非 UI 工作忙得太久,以至于用户能感觉出来?
一个很实用的判断方式是:
- 用户能感觉到,就说明值得测
- 主 isolate 在做非 UI 重活,就该拆
- 启动阶段如果为了“方便”把十件事串起来做,通常迟早要还债
一些视觉效果,成本比看起来高
下面这些老问题,到现在依然是高频性能来源:
saveLayer- 大范围 opacity
- 不必要的 clipping
- blur 和 image filter
- 在滚动场景里堆比较重的 shader 效果
这些东西不是不能用,而是别当成免费。
saveLayer
你不一定直接调用它,但第三方组件或者某些 widget 可能会间接触发。
离屏合成过多,在中端机上非常容易变成稳定掉帧源。
Opacity
给大子树套 Opacity,尤其是动画里这么做,成本通常比很多人预想得高。
Clipping
Clip 当然有用,但如果每个滚动 item 都在 clip,一层层叠上去,成本也会越来越明显。
如果只是做圆角,优先用 widget 自带的 borderRadius 能解决,就不要额外加一层 clip。
Blur 和 Filter
毛玻璃效果设计师都爱,但中低端设备通常会让你回到现实。
如果 blur 是核心视觉,尽早在目标设备上测,不要等到发版冲刺时才发现成本压不住。
核心原则不是“不许用效果”,而是:
只在真正值得的地方用。
启动性能也是产品质量的一部分
很多团队对运行时卡顿很敏感,但对启动慢很迟钝。
用户不是这样。
Flutter 启动慢,通常来自这些组合拳:
- 包体偏大
- 初始化做得太早
- 首帧前同步工作太多
- 还没进主界面就拉了很多暂时不需要的数据
- 依赖链太重
- 资源、字体、资产包得太多
几个值得反复问的问题:
- 首个可用页面之前,哪些事真的是必须做的?
- 哪些事情可以首帧后再做?
- 哪些可以 lazy load?
- 有没有打包了用户大概率根本碰不到的资源?
- 字体、locale、二进制是不是带了太多没必要的东西?
一个“技术上只慢两秒”的启动,如果首屏迟迟不出现,或者出来后马上再卡一次,用户体感依然会很差。
一定要在用户会用的设备上测
这话听起来像废话,但还是经常没人做。
模拟器和仿真器适合开发,不适合给性能结论。
至少应该覆盖:
- 一台偏慢的 Android 机
- 一台主流当前设备
- 目标刷新率对应的设备
- 图片和接口比较重的页面,在真实网络条件下跑一次
很多问题只会在真机上老老实实暴露出来:
- shader 卡顿
- 图片内存压力
- 热降频
- 启动回归
- 滚动时网络和图片解码叠加导致抖动
- 长时间使用后的 GC 停顿
如果你所有 profiling 都是在一台高配手机、插着电、连着开发机做的,那你优化的是一个过于理想的世界。
别只会救火,要把回归挡在前面
性能工作最便宜的时候,不是在事故之后,而是在正常研发流程里。
一些比较实际的习惯包括:
- 明确哪些页面是性能敏感页面
- 跟踪包体和启动体积趋势
- 对大的 UI 改动保留 before / after trace
- 列表页、动画页改动时顺手跑 profile 检查
- 功能开发时就一起评估图片策略
- code review 时把“这个改动会不会扩大 rebuild 范围”当成正常问题
不是每个团队都需要一套复杂的性能实验室。
大多数团队真正缺的,只是及时意识到“这个页面以前很稳,现在开始变脆了”。
一份实用的 review 检查单
每次审 Flutter 改动时,可以顺手问这几件事:
- 这次改动会不会扩大 rebuild 范围?
- 有没有把额外工作塞进
build()? - 页面是不是依赖大列表或复杂 grid?
- 图片尺寸、解码尺寸是不是合理?
- 有没有重活还跑在 UI isolate 上?
- layout 是不是依赖昂贵的 intrinsic 测量?
- 有没有新引入 opacity、clip、blur、saveLayer 相关成本?
- 启动阶段是不是又多做了初始化?
- 有没有在真机的 profile / release 模式下看过?
如果这里面好几项答案都是“有”,那性能就应该是这次变更讨论的一部分,而不是以后再说。
最后压缩成几句话
对 Flutter 3.41.6 / Dart 3.11.4 来说,性能优化最重要的基本功还是这些:
- 先测量,再下手
- 时刻尊重帧预算
- 把 rebuild 范围收小
- 列表和布局尽量做得可预测
- 把图片当成一整条链路来管
- 必要时把重活移出 UI isolate
- 视觉效果按价值付费
- 启动性能别装看不见
- 一定上真机测
- 把性能回归预防做成日常习惯
大多数 Flutter 性能问题都不神秘。
本质上就是很多小成本没人及时看见,最后一起堆成了用户可感知的卡顿。