性能监控——启动耗时、内存、帧率
标签:
Flutter性能监控启动优化内存治理帧率
这篇不讲“性能优化大法”,只讲我在业务项目里最后落下来的那套监控方法:
先把数据跑通,再谈优化动作。没有持续监控,任何优化都只是一次性的“感觉更快了”。
1. 问题背景:业务场景 + 现象
我们项目里最早的性能反馈,基本都来自两类:
- 用户口碑:“打开慢”“偶尔卡一下”“进房间有掉帧”
- 研发体感:本地调试没问题,上线后低端机明显卡
真正麻烦的是:
大家说的是“慢”,但慢在启动、首屏渲染、列表滑动、动画过渡,还是GC 抖动,没人说得清。
没有统一指标,排查就会陷入“我这台机器复现不了”。
2. 原因分析:核心原理 + 排查过程
启动慢常见来源
- 冷启动阶段同步初始化太多(配置、埋点、SDK、缓存预热全挤在
main()) - 首屏
build里做了重 IO 或重计算 - 首屏图片过大、字体和资源解码集中在第一帧附近
内存抖动常见来源
- 页面退出后对象未释放(
StreamSubscription/Controller/Timer) - 图片缓存策略过于激进
- 列表项复用差,导致短时间大量对象分配
帧率下降常见来源
- 单帧内做了过重布局或绘制
- 动画期间触发大面积
setState/ rebuild - 异步回调扎堆回主线程,造成 UI 线程拥塞
我们的排查顺序(固定流程)
- 先看启动时间:冷启动 P50 / P90 是否超阈值
- 再看卡顿:慢帧、超慢帧、掉帧峰值出现在什么页面
- 最后看内存:长会话后曲线是否持续上升、GC 是否异常频繁
顺序不要反。启动和帧率问题没定性前,直接抠“内存曲线漂不漂亮”,效率很低。
3. 解决方案:方案对比 + 最终选择
我们最后做的是“三层监控”,不是单点工具。
方案对比
- 只靠 DevTools 手工排查
优点:细;缺点:不能持续、不能覆盖线上 - 只看线上 APM 平台
优点:真实设备数据;缺点:本地复现路径不清楚 - 本地基线 + CI 回归 + 线上观测(最终方案)
优点:能发现、能拦截、能复盘,闭环完整
最终落地
A. 本地基线(开发阶段)
- 每个核心页面保留一次 profile 结果(机型、系统、构建号)
- 指标固定:启动耗时、平均 FPS、慢帧数、内存峰值
B. CI 回归(提测前)
- 关键路径跑一轮性能 smoke(冷启动 + 首页滑动 + 关键动画)
- 指标超阈值直接标黄或失败(不需要等线上报警)
C. 线上观测(发布后)
- 采样上报启动耗时、慢帧率、内存告警事件
- 按版本、机型、系统分桶看趋势,不看总平均值
4. 关键代码:最小必要代码片段
下面这几段是我自己长期保留的“够用版”。
4.1 启动耗时埋点(冷启动)
import 'dart:developer' as dev;
import 'package:flutter/widgets.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final sw = Stopwatch()..start();
// 只保留必要同步初始化;其他延后到首帧后
await initCriticalServices();
runApp(const MyApp());
WidgetsBinding.instance.addPostFrameCallback((_) {
sw.stop();
final launchMs = sw.elapsedMilliseconds;
dev.log('app_launch_ms=$launchMs', name: 'perf.startup');
reportMetric('app_launch_ms', launchMs);
});
}
关键点:
- 别把所有初始化都
await在runApp之前 - 首帧后再打点,数据更稳定,便于版本对比
4.2 帧率与卡顿采集(页面级)
import 'dart:developer' as dev;
import 'package:flutter/scheduler.dart';
class FrameJankObserver {
int slowFrames = 0;
int totalFrames = 0;
void start() {
SchedulerBinding.instance.addTimingsCallback(_onTimings);
}
void stop() {
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
final ratio = totalFrames == 0 ? 0 : slowFrames / totalFrames;
dev.log('slow_frame_ratio=$ratio', name: 'perf.fps');
reportMetric('slow_frame_ratio', ratio);
}
void _onTimings(List<FrameTiming> timings) {
for (final t in timings) {
totalFrames++;
final buildMs = t.buildDuration.inMilliseconds;
final rasterMs = t.rasterDuration.inMilliseconds;
if (buildMs > 16 || rasterMs > 16) {
slowFrames++;
}
}
}
}
关键点:
- 不纠结“平均 FPS 漂亮不漂亮”,先看慢帧比例和峰值位置
- 统计要按页面或场景隔离,否则定位不到问题点
4.3 生命周期与内存治理(避免后台恢复后抖动)
class AppLifecycleHandler with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.paused:
pauseHeavyTasks();
trimInMemoryCache();
break;
case AppLifecycleState.resumed:
resumeEssentialTasks();
break;
default:
break;
}
}
}
关键点:
- 回到前台时不要一口气恢复所有任务,分批恢复
- 低优先级任务(预加载、清理)放到空闲时段
5. 效果验证:数据/截图/日志
我们连续 3 个版本看趋势,最有价值的是这三个结论:
- 冷启动 P90 从 3.2s 降到 2.3s(首帧后初始化拆分贡献最大)
- 核心页面慢帧率从 8% 降到 3%(重建范围收敛 + 动画期减少重计算)
- 长会话内存峰值下降约 18%(页面退出时资源释放 + 图片缓存策略调整)
日志侧我只保留两类:
- 版本对比日志:
build_number + metric + device_tier - 异常样本日志:只记录超阈值样本,减少噪音和上报成本
6. 可复用结论:通用经验 + 避坑清单
通用经验
- 性能治理先做“监控工程”,再做“代码优化”
- 指标分层:启动、帧率、内存不要混成一个“性能分”
- 看趋势,不看单次截图;看分位数,不看平均值
- 每次优化只改一个变量,避免“多改叠加看不出因果”
避坑清单
- 用 debug 模式的数据讨论线上性能
- 只看高端机,不看中低端机分布
- 发布后不复盘版本差异,导致问题反复出现
- 发现慢帧就立刻重构,没先做采样定位
- 指标上报过多,最后没人看
我现在的执行习惯(团队可直接照搬)
- 每个版本冻结前,固定跑一次性能回归(同机型、同网络、同流程)
- 发布后一周盯三张图:冷启动分位、慢帧率、内存峰值
- 超阈值先建“性能问题卡”,不在群里靠口头追踪
- 复盘里只回答三件事:哪项指标变差、为什么、下版怎么防回归
下一篇我会写(5/6):发版 checklist:如何降低线上事故率,重点放在“哪些项必须阻断发版,哪些项可带风险上线”。