性能指标定义:你得先知道量什么
我个人在做动态化性能监控时,有一个习惯:先把指标定义敲死。不然后面数据出来了,大家吵起来,谁都不认账。
说白了,指标就像一把尺。尺子刻度不对,量出来全是废数据。今天我们就把这几个核心指标摸透:冷启动、热启动、首帧时间、FPS、CPU/内存占用。
我遇到过不少团队,启动速度优化做了大半年,结果发现冷启动和热启动的定义就没对齐。前端说是冷启动,后端觉得是热启动。嗯,这就是典型的问题。
1. 冷启动耗时
冷启动,指的是进程从零开始,到用户看到第一个可用界面的全过程。没有缓存、没有预加载、没有热数据。
我个人习惯把冷启动分成三个阶段:
- 进程初始化:应用进程创建,系统分配资源
- 框架加载:JS引擎初始化、动态化容器创建
- 首帧渲染:从数据请求到第一帧上屏
我在项目中遇到过很多冷启动的坑。比如,有个业务线的首页在冷启动时,光布局计算就花了800ms。你想想看,这800ms用户就在那干等。后来用Chrome DevTools的性能面板一查,发现是首次布局时,所有组件的样式规则都要重新解析一遍。
冷启动耗时标准
| 级别 | Android | iOS |
|---|---|---|
| 优秀 | < 2s | < 1.5s |
| 一般 | 2s - 4s | 1.5s - 3s |
| 较差 | > 4s | > 3s |
注意:冷启动必须包含网络请求时间!有次我看到上报的冷启动耗时只有1.2s,结果发现是数据还没请求完就上报了。这就是典型的定义问题。
2. 热启动耗时
热启动就简单多了。进程已经在后台存活,用户只是重新回到前台。说白了,就是从Application.onCreate之后到首帧渲染完成的时间。
我在监控热启动时,最关注的是两个东西:
- 资源命中率:本地缓存是否还在,模板是否需要重新请求
- 首帧复用率:上次渲染的视图是否可以直接复用
热启动理论上控制在200ms以内才算合格。我曾经遇到一个案例,热启动耗时780ms,排查发现是每次回到前台都要重新创建动态化容器。明明容器可以复用,代码里却写了销毁重建的逻辑。
这里有个小技巧:用adb shell dumpsys activity可以拿到进程状态。如果进程状态是STOPPED,那就是冷启动。如果STOPPED都没出现过,那就是热启动。
我个人习惯用打桩的方式在底层埋点。比如在动态化容器的onCreate和onViewCreated之间记录时间戳。这样能精确知道热启动的瓶颈在哪个环节。
3. 首帧时间
首帧时间,指的是从用户触发启动行为,到屏幕上出现第一帧画面的时间。这个指标最容易被人误解。
很多人觉得首帧时间等于页面渲染时间。不对。首帧时间只关心显示第一帧,不关心页面是否完整。哪怕只画了个背景色,那也是首帧。
我在项目中常用两种方式来测量首帧时间:
- 系统API法:iOS的
CADisplayLink,Android的FrameMetrics - 打桩法:在渲染管线的最前端和后端埋点
你想想看,首帧时间慢了,用户直观感受就是“点了一下没反应”。我曾经用500ms的首帧时间和1200ms的首帧时间做了A/B测试,用户留存率差了将近8%。
对了,首帧时间有一个坑:动态化场景下,首帧可能是容器本身的默认视图,不是业务内容。这意味着你看到的首帧时间很漂亮,但用户看到的是个白屏盒子。所以,我建议首帧时间从业务层面定义——真实内容的第一帧。
首帧时间测量说明
// Android 示例:使用 FrameMetrics
window.addOnFrameMetricsAvailableListener({ _, frameMetrics ->
val firstFrame = frameMetrics.getMetric(FrameMetrics.FIRST_DRAW_FRAME)
Log.d("FirstFrame", "首帧耗时: ${firstFrame / 1_000_000}ms")
}, Handler(Looper.getMainLooper()))
4. FPS(帧率)
FPS是一个老生常谈的指标,但我发现很多人只看平均值。平均值能骗过大多数人。你说你FPS是55,但中间有几帧卡了200ms,用户一样觉得卡。
我建议关注这三个维度:
- 平均FPS:整体性能水平
- 低帧率占比:FPS<30的帧数占总帧数的比例
- 帧间隔抖动:相邻帧之间的时间差,这个最能反映卡顿感
动态化场景下,FPS下滑最常见的原因有两个:
- JS线程阻塞:动态化容器的渲染依赖JS线程,一旦JS卡住,FPS直接断崖
- 布局重计算:动态化布局每次都要从模板解析成原生视图,这个计算很贵
我曾经在项目里遇到过FPS一直卡在25左右的问题。排查后发现是模板中用了大量嵌套的Flexbox,每次布局都要递归计算。后来改成扁平化布局,FPS直接跳到55。
用Choreographer可以监听帧回调。我习惯在回调里记录frameTimeNanos,然后计算相邻回调的时间差。超过16.6ms就是掉帧了。
5. CPU / 内存占用
CPU和内存,是性能监控里最基础的两个指标。但很多人忽略了一个重点:瞬态峰值比平均值更致命。
比如冷启动时,CPU可能在短时间内冲到80%以上,然后回落到10%。如果你只看平均CPU用量,是看不出这个波动对系统的影响的。
我建议这样监控:
| 指标 | 监控方式 | 参考红线 |
|---|---|---|
| CPU峰值 | 每100ms采样一次 | < 80% |
| CPU均值 | 启动开始到首帧后的1s | < 30% |
| 内存峰值 | 启动过程中最大瞬时内存 | < 总内存的40% |
| 内存泄漏 | 连续5次GC后内存不回缩 | 触发告警 |
动态化容器启动时,内存占用大多是模板解析和JS引擎的开销。我见过一个案例,模板文件有2MB,每次启动都要全部解析成AST树,光这一步内存就涨了150MB。后来改成懒加载+模板缓存,内存降到了30MB左右。
注意:不要把CPU和内存分开监控!它们之间有关联性。CPU飙升往往伴随内存分配加剧,这时候GC会被频繁触发,又反过来拖慢CPU。我曾经就因为只监控CPU,忽略了内存压力,结果定位到问题花了两周。
我个人总结的三个原则
- 指标定义必须在团队内达成一致,文档化、版本化管理
- 冷热启动要分开看,混在一起等于没看
- FPS和内存是联动指标,出了问题往往是一起出