性能优化与 DevTools

Flutter 的性能问题几乎都落在两个线程上:UI 线程负责 build 与布局,Raster 线程负责绘制。卡顿要么是 Dart 侧算得太慢,要么是 GPU 侧画得太贵,凭感觉猜十次不如用 DevTools 看一次帧图。本章先讲怎么测、怎么判读,再给出常见问题与修法的对照表,最后覆盖启动时间、内存与包体积。

先测量再优化

flutter run --profile            # 性能问题必须在 profile 模式复现,debug 模式数字无意义
flutter run --profile --trace-startup   # 记录启动阶段耗时,退出时打印时间线
flutter build apk --analyze-size --target-platform android-arm64  # 分析包体积构成
flutter build apk --release --split-debug-info=build/symbols     # 保留符号表,崩溃可符号化

Debug 模式里所有断言与 JIT 都在运行,滚动掉帧属于正常现象;任何性能结论都要在 profile(或 release)模式下得出。

DevTools 三大面板

面板看什么典型结论
Performance帧图(UI / Raster 两条柱)、jank 标记UI 柱高 = build+layout 超时;Raster 柱高 = 绘制过重
CPU Profiler火焰图与方法耗时占比找到最热的 Dart 方法,通常是不必要的重建或 JSON 解析
Memory堆快照、分配追踪、GC 事件内存持续上涨 = 泄漏;大图 = 解码尺寸过大

打开方式:flutter run 后在终端按 v,或用 DevTools 独立客户端连接;同时开启 Track widget buildsHighlight repaints 两个开关,重建与重绘会直接画在屏幕上,比看数字直观得多。

60/120fps 与掉帧判定

目标单帧预算判定标准
60 fps16.7 ms(UI + Raster 各自)帧图出现红色 jank 柱子即为掉帧
120 fps8.3 ms高刷屏上 16 ms 的帧也会被感知为不流畅
首帧越短越好冷启动白屏时长,用 --trace-startup 量化

判断责任方有个简单办法:UI 线程高说明 Dart 侧重建/布局太慢,去看 Track widget buildsRaster 线程高说明绘制复杂(模糊、阴影、裁剪、大图),去看 Highlight repaints 与内存面板。

常见问题与修法对照表

现象根因修法
整页频繁重建setState 挂在高层,const 缺失拆小组件、加 const、状态管理用 Provider.select 精确订阅
长列表滚动卡一次性构建全部子项、无固定高度ListView.builder,高度一致时加 itemExtent
列表图片内存高按原始尺寸解码大图Image.network(url, cacheWidth: 200) 限制解码尺寸
局部动画导致整屏重绘动画与静态内容在同一层动画区域加 RepaintBoundary
Opacity 做淡入淡出每次都触发 saveLayer,非常贵改用 AnimatedOpacity 或直接给颜色加 alpha
首屏白屏久main 里做了太多同步初始化延迟非必要初始化,先 runApp 再补数据
列表项有阴影/模糊绘制成本高用图片代替模糊阴影,或降低 elevation
// 1) const 化:构造函数是 const 的 Widget 不会因为父级重建而重建
const _header = Text('固定标题', style: TextStyle(fontSize: 18));

// 2) 列表只构建可见项,并给出固定高度让布局走快路径
ListView.builder(
  itemExtent: 88, // 高度一致时能省掉逐项测量
  itemCount: items.length,
  itemBuilder: (context, i) => ProductTile(item: items[i]),
);

// 3) 图片按显示尺寸解码,避免 4000px 原图占几十 MB 内存
Image.network(item.cover, width: 120, cacheWidth: 240, fit: BoxFit.cover);

// 4) 把不断重绘的动画与静态内容隔离开
RepaintBoundary(child: AnimatedChart(data: data));

启动时间

  • main 里只做必要的事:WidgetsFlutterBinding.ensureInitialized()、错误上报初始化、runApp。数据库预热、埋点 SDK、远程配置都放到首帧之后。
  • Future.microtask 或首帧回调延后初始化,避免和首帧抢时间。
  • Android 侧还可以检查 MainActivity 的启动主题与 flutter_engine 预热;iOS 侧关注 AppDelegate 里的同步操作。
  • 启动阶段默认就慢的场景(如需要登录态)应先渲染骨架屏,而不是等数据齐了再显示界面。

内存与泄漏

排查顺序:Memory 面板里连点两次 GC 再拍快照,进入并退出可疑页面,重复操作若干次后对比快照,看同类对象数量是否线性增长。常见泄漏点:AnimationController/StreamSubscription/Timer 没在 dispose 里释放;addListener 之后忘记 removeListener;闭包捕获了 BuildContext 并在异步回调里使用。DevTools 的 Memory Allocation 视图能直接定位到分配这些对象的调用栈。

包体积

手段效果代价
--split-per-abi单 ABI 包体积下降约 30%~40%需分别上传多个 APK
--split-debug-info产物中移除调试符号,明显变小必须保存符号表才能还原崩溃栈
--analyze-size输出各模块体积占比仅用于分析,不产出可发布包
移除未使用资源图标、字体、多语言包按需裁剪需要人工核对
字体子集化中文字体可省数 MB需重新生成字体文件

优化顺序建议:先用 --analyze-size 看清大头在哪里,再决定动手。多数项目的体积大头是引擎与字体资源,而不是业务代码。

验收指标与取舍

指标建议验收线说明
滚动流畅度列表连续滚动无 jank在最低端的目标机型上测,中端机达标说明不了问题
冷启动首帧 < 2 s(中端机)--trace-startup 量化,白屏时间要单独统计
内存反复进出页面后堆占用不增长快照对比法,允许有缓存上限但不允许线性上涨
包体积单 ABI APK 增量可控每次发版对比 --analyze-size 输出

优化的取舍比技巧更重要:

  • 用户无感的 200 ms 不值得为它引入复杂的懒加载框架,可维护性也是成本。
  • 包体积与开发速度通常不可兼得:为省 3 MB 砍掉国际化与埋点 SDK,只在工具类小应用里划算;业务应用优先保功能。
  • “越快越好”无法验收,把它换成上面表格里的具体数字,团队才有共同语言。
  • 优化代码要留痕:itemExtentcacheWidth 这类改动必须写注释说明为什么这么写,否则后人重构时会顺手删掉。

常见坑

现象正确做法
在 debug 模式下评估性能结论全是错的一律用 flutter run --profile
只优化不给指标改完不知道有没有变好优化前后各取一次帧图与耗时数据
满屏 RepaintBoundary图层过多,合成开销上升只包真正高频重绘的区域
过早使用复杂状态管理重建范围反而变大先用 const/setState,确有必要再上框架
忽略图片解码尺寸内存高、低端机闪退cacheWidth/cacheHeight
一次性做太多优化改动风险高、收益难衡量按帧图热点逐个击破

小结:性能优化的纪律是“profile 模式测量 → 看帧图定位 UI 还是 Raster → 按对照表修 → 再测一次对比”;重建问题靠 const、拆组件与精确订阅,长列表靠 ListView.builderitemExtent,重绘靠 RepaintBoundary,图片靠 cacheWidth;启动、内存与包体积分别用 --trace-startup、DevTools 快照对比与 --analyze-size 量化,不要凭感觉下手。

笔记加载中…