编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 体系建设优化

    • 体系建设优化
    • 性能第一性原理
    • 指标与求证方法
      • 01.为什么需要统一的指标体系
        • 1.1 指标混乱的代价
        • 1.2 一个好指标的五个特征
      • 02.三种指标视角
        • 2.1 USE 模型 — 资源视角
        • 2.2 RED 模型 — 请求视角
        • 2.3 APDEX / Web Vitals — 用户感知视角
        • 2.4 三视角组合使用
      • 03.分布与百分位
        • 3.1 为什么均值是性能领域的谎言
        • 3.2 百分位的物理含义
        • 3.3 直方图与 HDR 直方图
        • 3.4 抖动与稳定性指标
      • 04.跨平台核心指标矩阵
        • 4.1 CPU 类指标
        • 4.2 内存类指标
        • 4.3 渲染类指标
        • 4.4 网络类指标
        • 4.5 启动类指标
        • 4.6 稳定性类指标
      • 05.指标的边界与陷阱
        • 5.1 高 Goodhart's Law 风险指标
        • 5.2 平台口径差异
        • 5.3 采样误差与置信区间
      • 06.从指标到 SLO
        • 6.1 SLI / SLO / SLA 的关系
        • 6.2 错误预算
        • 6.3 端侧 SLO 的特殊性
      • 07 行业 SLO 基准对照
        • 7.1 启动时间基准
        • 7.2 渲染与卡顿基准
        • 7.3 内存与崩溃基准
        • 7.4 网络性能基准
        • 7.5 头部 App 公开过的真实数字
        • 7.6 反例:失败的优化案例
      • 一句话总结
      • 附录:03.性能求证方法论
      • 01.为什么需要"实验"
        • 1.1 经验性结论的不可靠性
        • 1.2 性能优化是一门可证伪的工程学
      • 02.实验的科学框架
        • 2.1 假设的可证伪性
        • 2.2 控制变量原则
        • 2.3 对照组与实验组
        • 2.4 重复性与可复现性
      • 03.实验设计七步法
        • 3.1 第一步:明确命题
        • 3.2 第二步:选择度量指标
        • 3.3 第三步:设计实验环境
        • 3.4 第四步:确定样本量
        • 3.5 第五步:执行采样
        • 3.6 第六步:统计分析
        • 3.7 第七步:边界探查
      • 04.基准测试(Benchmark)的陷阱
        • 4.1 编译器优化导致的"代码消失"
        • 4.2 缓存预热与冷启动
        • 4.3 测量本身扰动被测对象
        • 4.4 设备状态干扰
      • 05.线上 A/B 实验
        • 5.1 与线下基准的差异
        • 5.2 流量切分与样本平衡
        • 5.3 显著性检验
        • 5.4 副作用监控
      • 06.求证案例完整示范
        • 6.1 命题:图片预解码是否真的能减少首屏卡顿
        • 6.2 实验流程演示
        • 6.3 数据呈现规范
      • 07.求证报告模板
      • 一句话总结
    • 可观测性与归因
    • APM与数据治理
    • 性能预算与防劣化
  • 代码品质工坊

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

  • 性能优化实践

  • 真经
  • 体系建设优化
杨充
2026-06-27
目录

指标与求证方法

# 跨平台性能模型与指标体系

📊 学习成本预估 | 难度:⭐⭐⭐⭐(4/5)| 阅读:约 25 分钟 | 实操:1 小时 🔗 前置阅读:卷零·01 | ➡️ 后续延伸:卷一·01, 卷一·03

本文定义专栏的统一指标语言。所有性能议题(启动、卡顿、内存、网络…)必须能在本文定义的指标体系中找到对应位置,否则视为"伪议题"。

# 目录介绍

  • 01.为什么需要统一的指标体系
    • 1.1 指标混乱的代价
    • 1.2 一个好指标的五个特征
  • 02.三种指标视角
    • 2.1 USE 模型 — 资源视角
    • 2.2 RED 模型 — 请求视角
    • 2.3 APDEX / Web Vitals — 用户感知视角
    • 2.4 三视角组合使用
  • 03.分布与百分位
    • 3.1 为什么均值是性能领域的谎言
    • 3.2 百分位的物理含义
    • 3.3 直方图与 HDR 直方图
    • 3.4 抖动与稳定性指标
  • 04.跨平台核心指标矩阵
    • 4.1 CPU 类指标
    • 4.2 内存类指标
    • 4.3 渲染类指标
    • 4.4 网络类指标
    • 4.5 启动类指标
    • 4.6 稳定性类指标
  • 05.指标的边界与陷阱
    • 5.1 高 Goodhart's Law 风险指标
    • 5.2 平台口径差异
    • 5.3 采样误差与置信区间
  • 06.从指标到 SLO
    • 6.1 SLI / SLO / SLA 的关系
    • 6.2 错误预算
    • 6.3 端侧 SLO 的特殊性

# 01.为什么需要统一的指标体系

# 1.1 指标混乱的代价

设想以下真实对话:

  • A:我们启动优化了 30%!
  • B:你说的是冷启动还是温启动?
  • A:……都算吧,平均值。
  • B:哪一档机型?是 Time to First Frame 还是 Time to Interactive?
  • A:……

指标定义不清的结果:

  • 团队之间无法对齐 —— 同一个数字代表不同含义。
  • 优化效果不可验证 —— 改动前后的"对比"不可比。
  • 线上线下不一致 —— 实验室数据漂亮,线上灾难。
  • 老板看的报表是错的 —— 决策基于错觉。

统一指标体系的目的:让"启动 1.2s"这种描述对任何团队、任何平台都有精确、可复现、可度量的含义。

# 1.2 一个好指标的五个特征

借鉴 Google SRE 与 Brendan Gregg 的实践:

特征 含义 反例
可度量 可由仪器自动采集 "用户感觉很卡"
可定义 有数学表达,无歧义 "性能优秀"
可对比 不同时刻 / 不同版本之间可比 单次抽样
可归因 异常时能定位到具体层 "整体性能下降"
可决策 有阈值能驱动行动 "FPS 大概 50"

# 02.三种指标视角

性能指标的世界有三个互补的视角,缺一不可。

# 2.1 USE 模型 — 资源视角

由 Brendan Gregg 提出,针对任意资源的健康度三元组:

维度 含义 典型指标
Utilization 利用率 资源在被使用的时间占比 CPU% / 内存使用率 / GPU 占用率
Saturation 饱和度 排队 / 等待程度(超额需求) Run Queue 长度 / Page Fault 频率 / IO Wait
Errors 错误数 资源相关错误事件 OOM / IO Error / 网络重传

关键认知:

  • 利用率高 ≠ 有问题(CPU 100% 可能是健康的,只要不饱和、不出错)。
  • 饱和度才是性能瓶颈的真实信号。CPU 利用率 70% + Run Queue 持续 > 1,比 CPU 100% + Run Queue=0 更危险。

适用场景:CPU、内存、磁盘、网络、GPU、文件描述符、线程池…… 任何"有限资源"。

# 2.2 RED 模型 — 请求视角

由 Tom Wilkie 提出,针对任意请求型组件:

维度 含义 典型指标
Rate 速率 单位时间请求数 QPS / 启动次数 / 帧产出速率
Errors 错误率 失败请求占比 网络错误率 / 渲染丢帧率 / 崩溃率
Duration 时长 单次请求耗时分布 接口耗时 P95 / 帧时长 P99

适用场景:网络请求、页面加载、单帧渲染、单次启动、单次手势响应。

# 2.3 APDEX / Web Vitals — 用户感知视角

USE 看资源、RED 看请求,但用户不关心这些,用户关心感受。因此需要第三类指标:

# APDEX (Application Performance Index)

APDEX = (Satisfied + Tolerating/2) / Total

阈值 T 由产品定义:
  ≤ T          → Satisfied
  T < x ≤ 4T   → Tolerating
  > 4T         → Frustrated

# Google Web Vitals(Web 端事实标准,可借鉴到客户端)

指标 含义 优秀阈值 物理依据
LCP Largest Contentful Paint 主要内容渲染 < 2.5s 用户感知"加载完成"
INP Interaction to Next Paint 交互到下次绘制 < 200ms 操作反馈连续性
CLS Cumulative Layout Shift 布局抖动累积 < 0.1 视觉稳定性

# 客户端等价指标(本专栏定义)

客户端指标 Web Vitals 对应 含义
TTFM Time to First Meaningful Frame LCP 启动到首屏可见有意义内容
TTI Time to Interactive INP/TTI 启动到可响应用户操作
Tap Latency 点击响应延迟 INP 用户触摸到下一帧绘制
Frame Drop Ratio 丢帧率 — 单位时间内未按时绘制的帧占比
Jank Score 卡顿评分 — 综合卡顿严重度(次数 × 时长)

# 2.4 三视角组合使用

        用户感知(APDEX/Vitals)
              ▲
              │ "用户体验如何?"
              │
        请求时长/错误(RED)
              ▲
              │ "哪类操作出问题?"
              │
        资源饱和/错误(USE)
              ▲
              │ "为什么会出问题?"
              │
              系统层

自顶向下分析路径:

  1. 用户感知层(APDEX)告警 → 用户体验劣化。
  2. 下钻到 RED → 哪些请求慢 / 错?
  3. 下钻到 USE → 是哪个资源饱和 / 出错导致的?

反例:只看 USE 不看 APDEX —— "CPU 不高、内存不满,但用户骂街"。这种情况几乎一定是流水线阻塞(off-CPU stall),需要专门的方法(详见《05.归因方法论》)。

# 03.分布与百分位

# 3.1 为什么均值是性能领域的谎言

考虑两组延迟数据,单位 ms:

组 A: [50, 50, 50, 50, 50, 50, 50, 50, 50, 50]            均值 50, P99 50
组 B: [10, 10, 10, 10, 10, 10, 10, 10, 10, 410]           均值 50, P99 410

均值相同,但 B 的用户体验是灾难:每 10 次操作有 1 次卡顿 410ms。如果只看均值,你以为两个版本一样好。

为什么性能数据天然长尾?

  • GC 偶发触发
  • IO 偶发抖动
  • 缓存偶发未命中
  • 锁偶发竞争激烈
  • 网络偶发抖动

→ 性能延迟分布几乎都是右偏的、长尾的,均值被尾部样本严重拉偏。

# 3.2 百分位的物理含义

百分位 物理含义 关注场景
P50(中位数) 一半用户的体验 普遍体验
P90 10% 用户感受到的"差体验" 优化的常规目标
P95 5% 用户感受到的"明显差体验" 商业级 SLO 常用
P99 1% 用户的"灾难体验" 大流量产品必看
P99.9 / P99.99 万分之一概率的极端尾部 金融 / 关键系统

关键认知:

  • DAU 1 亿的产品,P99 = 5s 意味着每天 100 万次"灾难体验"。
  • 优化的目标不是降均值,而是压缩尾部。

# 3.3 直方图与 HDR 直方图

百分位需要从直方图计算。两种常见实现:

类型 优点 缺点 适用
等距直方图 简单 高百分位精度差 范围已知的指标
HDR Histogram 对数桶,全量程高精度 实现复杂 性能延迟(推荐)

实践要点:

  • 不要用"算 P99 = 取后 1% 平均" —— 这是错的。
  • 不要在客户端聚合 P99 后上报 —— P99 不可线性合并。
  • 上报直方图桶 + 计数,在服务端聚合后计算分位。

# 3.4 抖动与稳定性指标

均值低不代表稳定。以下指标度量"波动":

指标 含义 用途
标准差 σ 波动幅度 整体稳定性
变异系数 CV = σ/μ 相对波动 跨指标对比
P99/P50 比率 长尾扩张倍数 长尾健康度
Jitter 相邻帧 / 相邻请求时长差 帧率稳定性

帧率领域有句话:"60FPS 的均值不如 50FPS 的稳定"。用户对抖动的感知比对速度更敏感。

# 04.跨平台核心指标矩阵

下面是本专栏的"指标字典",后续章节直接引用。

# 4.1 CPU 类指标

指标 视角 定义 Android 采集 iOS 采集 Web 采集 嵌入式采集
进程 CPU 使用率 USE-U 进程占用 CPU 时间 / 总时间 /proc/[pid]/stat host_processor_info Performance API(受限) /proc/[pid]/stat
主线程 on-CPU% USE-U 主线程在 CPU 上执行时间占比 /proc/[pid]/task/[tid]/stat thread_info Long Tasks API task_struct
Run Queue 长度 USE-S 等待 CPU 的线程数 /proc/loadavg host_load_info 不可见 /proc/loadavg
上下文切换率 USE-S 每秒上下文切换次数 /proc/[pid]/status task_events_info 不可见 /proc/stat
IPC(每周期指令) 效率 单位 CPU 周期完成的指令数 simpleperf Instruments CPU Counters 不可见 perf

# 4.2 内存类指标

指标 视角 定义 Android iOS Web 嵌入式
RSS 物理内存 USE-U 实际占用物理内存 /proc/[pid]/status task_vm_info 不可见(隔离) /proc/[pid]/status
PSS 比例集 USE-U 共享内存按比例分摊后 /proc/[pid]/smaps 无对应 不可见 /proc/[pid]/smaps
Java/Heap 使用 USE-U VM 堆已用 Runtime.totalMemory() 无 performance.memory 无
GC 频率 USE-S 单位时间 GC 次数 ART trace 无(ARC) DevTools —
GC 停顿时长 USE-S 单次 GC 暂停时长 ART trace 无 PerformanceObserver(longtask) —
OOM 次数 USE-E 进程因内存被杀次数 lowmemorykiller Jetsam crash OOM Killer
内存抖动 USE-S 短时间内大量分配 / 释放 Allocation Tracker Allocations Instrument DevTools —

# 4.3 渲染类指标

指标 视角 定义
平均帧率 FPS RED-R 单位时间渲染帧数
帧时长 P99 RED-D 99% 的帧能在多长时间内绘制完成
丢帧率 RED-E 未在 Vsync 内完成的帧占比
大卡顿率 APDEX 单帧 > 700ms 的次数 / 总帧数
Jitter 抖动 相邻帧时长方差
GPU 占用 USE-U GPU 利用率
Overdraw 倍数 效率 像素被重复绘制的次数

平台采集:

  • Android:Choreographer.FrameCallback + gfxinfo + Perfetto
  • iOS:CADisplayLink + MetricKit + os_signpost
  • Web:requestAnimationFrame + PerformanceObserver(frame)
  • 嵌入式:显示控制器 IRQ + 帧缓冲交换计数

# 4.4 网络类指标

指标 视角 定义
请求成功率 RED-E 2xx + 3xx 占比
接口耗时 P95 RED-D 95% 请求完成时长
DNS / Connect / TLS / TTFB / Total 分段耗时 RED-D 请求各阶段时长
重传率 USE-S TCP 重传比例
弱网占比 环境 RTT > 阈值 / 丢包率 > 阈值的会话占比

# 4.5 启动类指标

启动是客户端最复杂的复合指标:

指标 起点 终点 物理含义
Cold Launch Time 进程创建 首帧 完整冷启动
TTFF Time to First Frame 进程创建 第一帧上屏 用户看到"动了"
TTFM Time to First Meaningful Frame 进程创建 首屏内容可见 用户看到"有用"
TTI Time to Interactive 进程创建 主线程空闲、可响应 用户能"操作"

三个时刻递进:TTFF < TTFM < TTI。多数团队只关注 TTFM,但 TTI 才决定"用户能不能用"。

# 4.6 稳定性类指标

指标 定义
Crash Rate 崩溃用户数 / DAU
ANR Rate ANR 用户数 / DAU
Foreground OOM Rate 前台 OOM 数 / DAU
异常退出率 非正常进程结束占比

# 05.指标的边界与陷阱

# 5.1 高 Goodhart's Law 风险指标

"When a measure becomes a target, it ceases to be a good measure." — Charles Goodhart

某些指标一旦被作为 KPI,团队会"针对指标"而非"针对体验"优化:

高风险指标 作弊方式
平均 FPS 把卡顿挪出采集窗口
启动时长 把耗时初始化推迟到首帧之后(伪首帧)
崩溃率 把 Crash 拦截后吞掉
内存均值 在采集时机主动 GC

对策:

  • 用多指标组合(FPS 必带 P99 + 大卡顿率)。
  • 用用户感知指标兜底(如 NPS、留存)。
  • 关键指标做采集口径文档化 + 评审。

# 5.2 平台口径差异

同名指标在不同平台口径不同,必须显式声明:

指标 Android 口径 iOS 口径 是否可直接对比
内存 PSS(含共享分摊) Footprint(不含 dirty 共享) ❌
FPS Choreographer 回调统计 CADisplayLink 回调统计 ✅(语义相近)
启动 Application.onCreate→首帧 main()→viewDidAppear ❌ 起止点不同
崩溃 包含 Native + Java + ANR 只含信号崩溃 ❌

# 5.3 采样误差与置信区间

任何指标都是样本估计,应附带置信区间:

P95 = 1.2s  ±0.05s  (95% CI, n=10000)

经验法则:

  • 估计 P50:样本量 ≥ 100
  • 估计 P95:样本量 ≥ 1000
  • 估计 P99:样本量 ≥ 10000
  • 估计 P99.9:样本量 ≥ 100000

样本不足时,禁止给出高百分位结论。

# 06.从指标到 SLO

# 6.1 SLI / SLO / SLA 的关系

概念 含义 例子
SLI Service Level Indicator 实际度量值 "本周冷启动 P95 = 2.1s"
SLO Service Level Objective 内部目标 "冷启动 P95 ≤ 2.0s 占比 ≥ 99%"
SLA Service Level Agreement 对外承诺 客户端通常无 SLA

# 6.2 错误预算

错误预算 = 1 - SLO

如 SLO 为 P95 ≤ 2.0s 占比 99%,
则允许 1% 的"违约时段"作为创新 / 上线的预算。

错误预算的两个用途:

  • 预算未耗尽:可激进上线、可做有风险的优化实验。
  • 预算耗尽:冻结新功能、专注治理。

# 6.3 端侧 SLO 的特殊性

服务端 SLO 主要看可用性,端侧 SLO 必须分维度切片:

切片维度 必要性
机型档位(高 / 中 / 低端) 高端机 SLO 严格,低端机宽松
系统版本 老系统给宽松目标
网络类型(WiFi / 4G / 弱网) 弱网下放宽
国家 / 地区 海外网络环境差异大
应用版本 防止新版本劣化老用户

端侧 SLO 的常见错误:用一个总均值定 SLO。结果是高端机被低端机数据掩盖、问题永远找不到。

# 07 行业 SLO 基准对照

本章汇集业界公开过的性能基准数据,作为团队制定 SLO 时的参考锚点。 数据来源:Google I/O / WWDC / 字节火山引擎 / 阿里云栖 / Meta 工程博客等公开材料。 使用注意:业界基准 ≠ 你的目标,你应该比所在行业平均高一档,比头部低一档(除非你就是头部)。

# 7.1 启动时间基准

类型 行业平均 优秀基准 头部目标 来源
Android 冷启动(中端机) 2.5-3.5s ≤ 2.0s ≤ 1.2s Google Play Console 公开
iOS 冷启动 1.5-2.5s ≤ 1.5s ≤ 0.8s WWDC 'Optimize App Launch'
Web 首屏(FCP) 2.5s ≤ 1.8s ≤ 1.0s Google Web Vitals
微信小程序冷启动 3-5s ≤ 2.0s ≤ 1.5s 微信公开课

# 7.2 渲染与卡顿基准

指标 行业平均 优秀基准 头部目标
FPS P95(60Hz) ≥ 50 ≥ 55 ≥ 58
FPS P99(60Hz) ≥ 40 ≥ 50 ≥ 55
卡顿率(帧 > 100ms) ≤ 2% ≤ 0.5% ≤ 0.1%
ANR 率 ≤ 0.4% ≤ 0.1% ≤ 0.05%
输入响应 P95 ≤ 200ms ≤ 100ms ≤ 50ms

# 7.3 内存与崩溃基准

指标 行业平均 优秀基准 头部目标
崩溃率 ≤ 0.5% ≤ 0.1% ≤ 0.02%
OOM 占崩溃比 30-50% < 20% < 10%
Java 堆峰值(中端机) 200-300MB ≤ 200MB ≤ 150MB
Native 堆 < 200MB < 150MB < 100MB

# 7.4 网络性能基准

指标 行业平均 优秀基准 头部目标
API 成功率 ≥ 98% ≥ 99.5% ≥ 99.9%
API 耗时 P95 ≤ 2s ≤ 1s ≤ 500ms
DNS 解析 P95 ≤ 200ms ≤ 100ms ≤ 50ms
弱网(地铁)成功率 ≥ 80% ≥ 95% ≥ 99%

# 7.5 头部 App 公开过的真实数字

App 场景 数字 来源
抖音 启动到首条视频可播 < 1s 字节火山引擎案例
微信 冷启动 P50 ~ 1.5s 微信公开课
手淘 冷启动到首屏 ~ 1.8s 阿里云栖大会
Instagram iOS 冷启动 < 1.2s Meta 工程博客
Notion 离线优先架构下,启动 P95 ~ 800ms Notion Engineering 博客

# 7.6 反例:失败的优化案例

案例 教训
Meta Lite 启动从 2.5s 优化到 0.8s 包体积增加 30%,发达地区收益 < 落后地区
某电商 App 全量异步化启动 启动反升 250ms(异步切换开销),后续回滚
Twitter "Predict and Preload" 实验 命中率仅 23%,整体功耗反而升高

核心启示:基准是参考,不是目标——你的业务、用户群、设备分布决定了你"应该"达到什么水平。

# 一句话总结

指标体系的价值,不是产生数字,而是产生"可决策的语言"。
当团队所有人在说"P95 帧时长"时,指的是同一件事,性能工程才能开始运转。


# 附录:03.性能求证方法论

# 性能求证实验方法论

📊 学习成本预估 | 难度:⭐⭐⭐⭐⭐(5/5)| 阅读:约 30 分钟 | 实操:2 小时 🔗 前置阅读:卷零·01-02 | ➡️ 后续延伸:所有专项卷

本文是专栏"科学家求证"风格的核心。任何"优化建议"在写入文档前,必须经过本文定义的实验流程产出可证伪的数据。否则视为经验主义,不予采纳。

# 目录介绍

  • 01.为什么需要"实验"
    • 1.1 经验性结论的不可靠性
    • 1.2 性能优化是一门可证伪的工程学
  • 02.实验的科学框架
    • 2.1 假设的可证伪性
    • 2.2 控制变量原则
    • 2.3 对照组与实验组
    • 2.4 重复性与可复现性
  • 03.实验设计七步法
    • 3.1 第一步:明确命题
    • 3.2 第二步:选择度量指标
    • 3.3 第三步:设计实验环境
    • 3.4 第四步:确定样本量
    • 3.5 第五步:执行采样
    • 3.6 第六步:统计分析
    • 3.7 第七步:边界探查
  • 04.基准测试(Benchmark)的陷阱
    • 4.1 编译器优化导致的"代码消失"
    • 4.2 缓存预热与冷启动
    • 4.3 测量本身扰动被测对象
    • 4.4 设备状态干扰
  • 05.线上 A/B 实验
    • 5.1 与线下基准的差异
    • 5.2 流量切分与样本平衡
    • 5.3 显著性检验
    • 5.4 副作用监控
  • 06.求证案例完整示范
    • 6.1 命题:图片预解码是否真的能减少首屏卡顿
    • 6.2 实验流程演示
    • 6.3 数据呈现规范
  • 07.求证报告模板

# 01.为什么需要"实验"

# 1.1 经验性结论的不可靠性

性能领域充斥着"民间智慧",例如:

  • ❓ "ArrayList 比 LinkedList 快"
  • ❓ "局部变量比成员变量访问快"
  • ❓ "RecyclerView 比 ListView 快"
  • ❓ "WebP 一定比 JPG 省流量"
  • ❓ "线程池一定比 new Thread 快"

这些结论部分情况下成立,但作为"通用建议"是危险的:

  • ArrayList vs LinkedList:取决于访问模式(随机访问 vs 频繁插入)。
  • RecyclerView vs ListView:在数据量小时,RecyclerView 的 ViewHolder 创建开销反而更高。
  • WebP vs JPG:解码 CPU 开销显著高于 JPG,弱机型下"省了流量、卡了体验"。
  • 线程池 vs new Thread:高频短任务确实如此,但单次长任务两者无差异。

结论的有效性永远依赖前提条件。求证实验的目的,就是把这些前提条件显式量化。

# 1.2 性能优化是一门可证伪的工程学

引入卡尔·波普尔(Karl Popper)的科学哲学:

一个命题是科学的,当且仅当它可被证伪。

反例(不可证伪):

  • "异步加载提升性能" —— 提升多少?什么场景下成立?无法证伪。

正例(可证伪):

  • "在 Android 8.0+、4GB 内存机型上,将首页 12 张图片改为 IO 线程预解码,首屏 P95 帧时长从 32ms 降至 18ms,置信区间 ±1.5ms" —— 任何人可重复实验,发现不符即证伪。

本专栏所有"建议"都必须以可证伪的形式陈述。

# 02.实验的科学框架

# 2.1 假设的可证伪性

实验从假设出发。一个合格的性能假设包含 5 个要素:

H₁: [变更 X] 在 [场景 S] 下,会使 [指标 M] 改善 [幅度 Δ],置信水平 [C]。

示例对比:

不合格假设 问题
"用 ProtoBuf 替换 JSON 会更快" 缺少场景、指标、幅度
"ProtoBuf 比 JSON 快 50%" 缺少场景、指标、置信水平
✅ "在 1000 字段大对象的反序列化上,ProtoBuf 较 Gson 的 P95 耗时降低 40%(±5%,95% CI),样本 n=10000" 完整

配套零假设 H₀:

H₀: 变更 X 对指标 M 没有显著影响(差异由随机波动产生)

实验目标是用数据拒绝 H₀,而非"证明 H₁"。

# 2.2 控制变量原则

只允许一个变量变化,其他变量必须严格一致。

典型违规:

  • "我升级了 SDK 版本,顺便重构了缓存策略,性能提升了 20%" —— 无法判断是哪个变更带来的提升,甚至可能两个变更互相抵消。

控制变量清单(性能实验必查):

  • [ ] 设备型号、SoC、内存容量
  • [ ] 系统版本(OS Build 号)
  • [ ] 应用版本、Build Type(Debug / Release)
  • [ ] 编译选项(-O0 / -O2 / R8 等级)
  • [ ] 充电状态、温度(移动设备会触发降频)
  • [ ] 网络类型 / 模拟弱网参数
  • [ ] 后台进程、内存压力
  • [ ] 数据集(图片、列表数据)
  • [ ] 启动状态(冷 / 温 / 热)

# 2.3 对照组与实验组

最少需要两组:

组别 含义
对照组(Control) 不做变更(baseline)
实验组(Treatment) 应用待测变更

进阶:

  • 多实验组:变更 A vs 变更 B vs 不变更,找出最优。
  • 交叉实验:同一台设备先跑 A 后跑 B,再换 B → A,消除设备序列偏差。

反例:"改完代码跑了一下,比之前快了 200ms" —— 没有对照组,无法排除环境差异。

# 2.4 重复性与可复现性

重复性(Repeatability):同一实验者、同一环境,多次运行结果一致。 可复现性(Reproducibility):他人按文档复刻实验,结果一致。

最低要求:

  • 单次实验内部,至少重复 30 次取分布。
  • 整体实验,至少在 2 台同型号设备上独立验证。
  • 关键结论,鼓励他人复刻(开源样例工程)。

# 03.实验设计七步法

# 3.1 第一步:明确命题

写下一句话命题,用 [01.总论] 的"四步归因"反推:

现象:用户反馈滚动列表卡
   ↓
命题候选:
  - H₁ₐ: 图片解码在主线程,导致单帧 > 16ms
  - H₁ᵦ: ViewHolder 创建过于频繁
  - H₁ᵧ: 业务回调里有同步 IO
   ↓
选择最可能的一个作为本实验命题

一次实验只验证一个命题。多命题混跑等于没跑。

# 3.2 第二步:选择度量指标

参考 [02.指标体系] 选择主指标 + 副指标 + 护栏指标:

类型 作用 示例(首屏卡顿实验)
主指标 直接验证假设 首屏 5s 内丢帧率
副指标 辅助理解机制 主线程 on-CPU%、IO Wait
护栏指标 防止顾此失彼 首屏内存峰值、CPU 总耗时

护栏指标的作用:避免"优化了主指标,但其他指标劣化"。

# 3.3 第三步:设计实验环境

环境必须稳定且接近真实:

维度 推荐设置
设备 至少高 / 中 / 低三档机型
系统 主流版本 + 最低支持版本
编译 Release 包(与线上一致)
监控 关闭其他采集 SDK,避免互相干扰
状态 充电至 50% 以上、温度 < 35°C、清空后台

线下复现真实环境的技巧:

  • 模拟弱网:Charles / 系统 Network Link Conditioner
  • 模拟内存压力:ActivityManager.setProcessImportance / memory_pressure_simulator
  • 模拟低端机:CPU 限频、关闭核心、ThrottlingOptions

# 3.4 第四步:确定样本量

样本量决定结论的统计力(Statistical Power)。

经验法则(参见 [02.指标体系] 5.3):

目标 最小样本量
估计均值 n ≥ 30
估计 P95 n ≥ 1000
估计 P99 n ≥ 10000
显著性检验(中等效应量) n ≥ 100 / 组

正式公式(双样本 t 检验):

n = 2 × (Z_{α/2} + Z_β)² × σ² / Δ²

其中:
  Z_{α/2} —— 显著性水平(α=0.05 时为 1.96)
  Z_β     —— 统计力(β=0.2 即 power=0.8 时为 0.84)
  σ       —— 指标标准差(先做 pilot 估计)
  Δ       —— 期望检测的最小差异

不知道 σ?做一次 30 样本的预实验估计一下,再算正式样本量。

# 3.5 第五步:执行采样

采样规范:

  1. 预热:先跑 N 次丢弃数据(消除冷启动 / JIT 编译影响)。
  2. 随机化:交替执行对照组与实验组,避免时间漂移(设备温度变化等)影响一组。
  3. 隔离:每次实验后清理状态(清缓存、重启应用),保证起点一致。
  4. 记录原始数据:保留每次原始耗时,不要直接保存均值。

典型采样脚本结构:

def run_experiment(group, n_warmup=10, n_sample=100):
    samples = []
    for _ in range(n_warmup):
        run_once(group)  # 丢弃
    for _ in range(n_sample):
        reset_state()
        t = run_once(group)
        samples.append(t)
    return samples

# 交替执行避免时间漂移
results_a, results_b = [], []
for _ in range(rounds):
    if random.random() < 0.5:
        results_a += run_experiment("A")
        results_b += run_experiment("B")
    else:
        results_b += run_experiment("B")
        results_a += run_experiment("A")

# 3.6 第六步:统计分析

必做的四件事:

# A. 描述性统计

组 A: n=1000, mean=18.2ms, P50=17ms, P95=24ms, P99=38ms, σ=4.1
组 B: n=1000, mean=14.5ms, P50=14ms, P95=19ms, P99=29ms, σ=3.5

# B. 可视化分布

  • 直方图 / 密度曲线:看分布形状(是否双峰、是否长尾)。
  • 箱线图:看四分位与离群点。
  • CDF 曲线:跨组对比时最直观。

# C. 显著性检验

数据特性 推荐检验
样本量大、近似正态 t 检验
长尾分布(性能数据常见) Mann-Whitney U 检验(非参数)
多组对比 Kruskal-Wallis

判定:p < 0.05 即拒绝 H₀,认为差异显著。

# D. 效应量

显著 ≠ 重要。还要看效应大小:

Cohen's d = (μ_A - μ_B) / σ_pooled

  d ≥ 0.2  小效应
  d ≥ 0.5  中等效应
  d ≥ 0.8  大效应

"显著且效应量大"才值得上线。"显著但效应量极小"是统计噪声。

# 3.7 第七步:边界探查

任何结论都有适用边界。明确以下问题:

  • 在哪些条件下结论不再成立?(机型 / 数据规模 / 网络条件)
  • 是否存在反例场景?
  • 是否会与其他优化冲突?(如预解码可能与内存压力冲突)

记录到结论的"适用边界"章节。没有边界声明的结论就是错的。

# 04.基准测试(Benchmark)的陷阱

线下做微基准(micro-benchmark)时,以下陷阱极易出错:

# 4.1 编译器优化导致的"代码消失"

// 想测试 expensiveCalc 耗时
long start = System.nanoTime();
for (int i = 0; i < 1_000_000; i++) {
    expensiveCalc(i);
}
long cost = System.nanoTime() - start;

如果 expensiveCalc 的返回值未被使用,编译器(JIT / R8 / LLVM)可能完全消除调用,测出几乎 0ms。

对策:

  • 使用专业框架:JMH(Java)、XCTest measure(iOS)、Benchmark.js(Web)
  • 强制使用返回值(Blackhole.consume(result))
  • 关闭可能影响测量的优化等级

# 4.2 缓存预热与冷启动

第一次调用慢(JIT 未编译、Cache Miss、Page 未加载),后续快。

对策:

  • 区分冷态和热态测试,分别上报。
  • 预热样本(warmup)丢弃。

# 4.3 测量本身扰动被测对象

Heisenberg 效应:

  • System.nanoTime() 调用本身有开销。
  • 频繁打日志拖慢被测路径。
  • Profiler 插桩使代码慢 5-10 倍(但比例可能改变结论)。

对策:

  • 测量调用尽量稀疏,外层包大循环平摊开销。
  • 使用低开销采样(如 perfetto sampling profiler)。
  • 测量"绝对值"用低开销工具,测量"相对差异"才用 profiler。

# 4.4 设备状态干扰

  • 移动设备温度上升触发降频(CPU 时钟降低 30%+)。
  • 后台进程抢占 CPU / IO。
  • 电池低于 20% 触发省电模式。

对策:

  • 实验前检查温度、电量、后台进程。
  • 单次实验时长 < 10 分钟,避免温度漂移。
  • 同设备多组实验间隔休息(降温)。

# 05.线上 A/B 实验

线下基准能验证"机制层"的因果,但最终需要线上 A/B 验证用户层效果。

# 5.1 与线下基准的差异

维度 线下基准 线上 A/B
控制度 高(可控制变量) 低(环境多样)
真实性 低(数据集理想) 高(真实用户)
样本量 千级 万级到亿级
反馈速度 分钟级 天级
关注 机制是否成立 业务指标是否真改善

最佳实践:先线下验证机制成立,再线上验证业务有效。

# 5.2 流量切分与样本平衡

  • 随机切分:基于稳定 hash(用户 ID)保证用户在各组的稳定性。
  • 同质性检查(A/A 实验):上线前先做 A/A 实验,确认两组在没有变更时也无显著差异。若有差异,说明分流或采集存在偏差。
  • 切片对齐:实验组与对照组在机型、地域、版本等维度上的分布要一致。

# 5.3 显著性检验

线上由于样本量极大,几乎任何差异都会"显著",因此重点不是 p 值,而是:

  • 置信区间:差值的 95% CI 是否完全在期望方向上。
  • 效应量:差异是否值得(业务上有意义)。
  • 持续时长:实验跑够 1 个完整业务周期(至少 7 天,覆盖工作日 / 周末)。

# 5.4 副作用监控

性能优化常见的"按下葫芦浮起瓢":

优化目标 可能的副作用
启动时长 ↓ 内存峰值 ↑、首屏崩溃 ↑(资源加载竞争)
内存占用 ↓ CPU ↑(更频繁 GC、解码)、卡顿 ↑
包体积 ↓ 启动 / 首屏耗时 ↑(动态加载)
网络流量 ↓ CPU ↑(解压)、首屏耗时 ↑

护栏指标必须配齐:上线前明确"哪些指标劣化超过 X% 即回滚"。

# 06.求证案例完整示范

下面用一个真实风格的案例演示完整流程。

# 6.1 命题:图片预解码是否真的能减少首屏卡顿

背景:列表页首屏存在 12 张图片,发现首屏 P95 帧时长 32ms,疑似图片解码占主线程。

初步假设:

H₁: 在主线程外预解码图片,首屏 P95 帧时长降低 ≥ 30%
H₀: 预解码对首屏 P95 帧时长无显著影响

# 6.2 实验流程演示

# Step 1 - 度量基线

指标类型 指标 当前值
主指标 首屏 5s 内 P95 帧时长 32ms
副指标 主线程 on-CPU% 78%
副指标 解码耗时占帧时长比 41%
护栏指标 首屏内存峰值 86MB
护栏指标 总解码 CPU 耗时 920ms

# Step 2 - 设计实验

项 配置
设备 A 机:旗舰;B 机:中端;C 机:低端,各 2 台
系统 Android 12 / 14;iOS 16 / 17
编译 Release,相同混淆等级
数据集 固定 12 张图片(300x400,JPG),首次进入清缓存
控制变量 充电、35°C 以下、关闭其他 App
样本量 每机型 / 每组 100 次,共 600 样本

# Step 3 - 采样

对照组(主线程解码):旗舰 100 / 中端 100 / 低端 100
实验组(IO 线程预解码):旗舰 100 / 中端 100 / 低端 100
交替执行,每组之间间隔 10 秒等待降温。

# Step 4 - 数据呈现

总体(n=600):
  对照组 P95 帧时长:32.4ms ±1.1ms
  实验组 P95 帧时长:18.7ms ±0.9ms
  改善幅度:-42.3%
  Mann-Whitney U 检验 p < 0.001(显著)
  Cohen's d = 1.34(大效应)

按机型切片:
  旗舰:32 → 14 ms (-56%)
  中端:33 → 19 ms (-42%)
  低端:32 → 22 ms (-31%)  ← 改善最小,因 IO 线程也慢

# Step 5 - 护栏检查

指标 对照组 实验组 变化 是否可接受
内存峰值 86MB 112MB +30% ⚠️ 低端机风险
总解码 CPU 耗时 920ms 950ms +3% ✅
首屏崩溃率 0.02% 0.04% +100% 相对 ⚠️ 需排查

# Step 6 - 边界探查

✅ 在 ≥ 4GB 内存设备上,结论稳定成立
⚠️ 在 2GB 内存机型上,预解码导致内存压力,反而触发更多 GC,反向劣化
❌ 在 1GB 机型上,可能触发 OOM

# Step 7 - 结论

对外结论:
"在内存 ≥ 4GB 的 Android / iOS 设备上,将首屏 12 张图片改为
IO 线程预解码,可使首屏 P95 帧时长降低 40%-56%(95% CI),
代价是内存峰值 +30%。
不适用于 ≤ 2GB 机型,需通过 Build.MODEL 黑名单兜底。"

# 6.3 数据呈现规范

实验结论必须包含以下结构(任何环节缺失都视为不合格):

  • ✅ 假设 H₁ / H₀
  • ✅ 实验环境(机型 / 系统 / 编译 / 数据集)
  • ✅ 样本量与重复次数
  • ✅ 主指标 + 副指标 + 护栏指标 的前后数据(带 P50/P95/P99 + 置信区间)
  • ✅ 显著性检验(p 值)+ 效应量(Cohen's d)
  • ✅ 适用边界(什么条件下不成立)
  • ✅ 上线策略(灰度比例 / 回滚指标)

# 07.求证报告模板

以下模板可直接复制到具体优化文档中:

## 求证报告:[变更名称]

### 一、命题
- H₁:[5 要素假设]
- H₀:[零假设]

### 二、实验设计
| 项 | 值 |
|---|---|
| 设备 |  |
| 系统版本 |  |
| 编译配置 |  |
| 数据集 |  |
| 控制变量 |  |
| 样本量 |  |
| 重复次数 |  |

### 三、指标
- 主指标:
- 副指标:
- 护栏指标:

### 四、原始数据
(直方图 / CDF 图 / 数据表)

### 五、统计分析
- 描述性统计(mean / P50 / P95 / P99 / σ)
- 显著性检验:(方法 + p 值)
- 效应量:(Cohen's d)

### 六、护栏检查
| 护栏指标 | 阈值 | 实测 | 是否通过 |
|---|---|---|---|

### 七、边界
- 适用:
- 不适用:

### 八、结论与上线建议
- 结论:
- 上线策略:
- 回滚指标:

# 一句话总结

没有实验的优化是巫术,没有边界的结论是谎言。
性能工程师的核心能力,不是"想到办法",而是**"证明办法"**。

上次更新: 2026/06/28, 17:55:19
性能第一性原理
可观测性与归因

← 性能第一性原理 可观测性与归因→

最近更新
01
科学方法实践论法
06-28
02
辩证思维矛盾论法
06-28
03
毛选中的调查观念
06-28
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式