质量与交付篇(4/6):性能监控——启动耗时、内存、帧率

0 阅读5分钟

性能监控——启动耗时、内存、帧率

标签:Flutter 性能监控 启动优化 内存治理 帧率

这篇不讲“性能优化大法”,只讲我在业务项目里最后落下来的那套监控方法:
先把数据跑通,再谈优化动作。没有持续监控,任何优化都只是一次性的“感觉更快了”。


1. 问题背景:业务场景 + 现象

我们项目里最早的性能反馈,基本都来自两类:

  • 用户口碑:“打开慢”“偶尔卡一下”“进房间有掉帧”
  • 研发体感:本地调试没问题,上线后低端机明显卡

真正麻烦的是:
大家说的是“慢”,但慢在启动首屏渲染列表滑动动画过渡,还是GC 抖动,没人说得清。
没有统一指标,排查就会陷入“我这台机器复现不了”。


2. 原因分析:核心原理 + 排查过程

启动慢常见来源

  • 冷启动阶段同步初始化太多(配置、埋点、SDK、缓存预热全挤在 main()
  • 首屏 build 里做了重 IO 或重计算
  • 首屏图片过大、字体和资源解码集中在第一帧附近

内存抖动常见来源

  • 页面退出后对象未释放(StreamSubscription / Controller / Timer
  • 图片缓存策略过于激进
  • 列表项复用差,导致短时间大量对象分配

帧率下降常见来源

  • 单帧内做了过重布局或绘制
  • 动画期间触发大面积 setState / rebuild
  • 异步回调扎堆回主线程,造成 UI 线程拥塞

我们的排查顺序(固定流程)

  1. 先看启动时间:冷启动 P50 / P90 是否超阈值
  2. 再看卡顿:慢帧、超慢帧、掉帧峰值出现在什么页面
  3. 最后看内存:长会话后曲线是否持续上升、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);
  });
}

关键点:

  • 别把所有初始化都 awaitrunApp 之前
  • 首帧后再打点,数据更稳定,便于版本对比

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. 可复用结论:通用经验 + 避坑清单

通用经验

  1. 性能治理先做“监控工程”,再做“代码优化”
  2. 指标分层:启动、帧率、内存不要混成一个“性能分”
  3. 看趋势,不看单次截图;看分位数,不看平均值
  4. 每次优化只改一个变量,避免“多改叠加看不出因果”

避坑清单

  • 用 debug 模式的数据讨论线上性能
  • 只看高端机,不看中低端机分布
  • 发布后不复盘版本差异,导致问题反复出现
  • 发现慢帧就立刻重构,没先做采样定位
  • 指标上报过多,最后没人看

我现在的执行习惯(团队可直接照搬)

  • 每个版本冻结前,固定跑一次性能回归(同机型、同网络、同流程)
  • 发布后一周盯三张图:冷启动分位、慢帧率、内存峰值
  • 超阈值先建“性能问题卡”,不在群里靠口头追踪
  • 复盘里只回答三件事:哪项指标变差、为什么、下版怎么防回归

下一篇我会写(5/6):发版 checklist:如何降低线上事故率,重点放在“哪些项必须阻断发版,哪些项可带风险上线”。