编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 性能优化实践

    • README
    • 公共方法论

    • 体系建设篇

    • 资源专项篇

    • 流水线专项

      • 渲染管线与原理
      • FPS与帧率检测
      • 卡顿捕获与归因
      • ANR监控与治理
      • 页面UI与布局优化
      • 动画交互响应优化
        • 01.阅读说明
        • 02.贯穿案例
          • 2.1 案例背景
          • 2.2 经验派的 3 周折腾(典型反面教材)
          • 2.3 方法派的 5 天闭环
          • 2.4 上线效果
          • 2.5 案例如何串起本文
        • 03.用户感知本质
          • 3.1 一句话定义
          • 3.2 现象与代价
          • 3.3 度量准则与基准
          • 3.4 反直觉问题清单
        • 04.交互链路原理
          • 4.1 交互延迟分段模型
          • 4.2 三个共识
          • 4.3 跨平台同构原理
          • 4.4 平台差异点矩阵
        • 05.度量与采集
          • 5.1 三类采集方案
          • 5.2 各方案的可见盲区
          • 5.3 跨平台采集对照表
          • 5.4 数据可信度评估
        • 06.归因决策树
          • 6.1 动画卡顿决策树
          • 6.2 交互延迟分段归因
          • 6.3 强制同步布局检测
          • 6.4 GPU 合成压力归因
        • 07.输入链路全链路
          • 7.1 Android 输入链路全链路
          • 7.2 iOS 输入链路全链路
          • 7.3 Web 输入链路全链路
          • 7.4 输入响应优化路径
          • 7.5 跨端输入响应性能数据
        • 08.动画驱动全链路
          • 8.1 Android 动画驱动全链路
          • 8.2 iOS Core Animation 全链路
          • 8.3 Web 动画驱动全链路
          • 8.4 高刷适配(60Hz → 120Hz)
          • 8.5 跨端动画驱动对照
        • 09.合成器全链路
          • 9.1 合成器物理原理
          • 9.2 哪些操作走合成器
          • 9.3 layer 化机制
          • 9.4 层爆炸的真实代价
          • 9.5 跨端合成器对照
        • 10.高频输入全链路
          • 10.1 滚动事件全链路
          • 10.2 输入框打字全链路
          • 10.3 拖动 / 缩放手势全链路
          • 10.4 高频输入优化策略
          • 10.5 跨端高频输入对照
        • 11.跨端动画对照
          • 11.1 各平台动画体系
          • 11.2 跨端动画性能对照
          • 11.3 各平台最佳实践
          • 11.4 反直觉问题答疑
        • 12.跨端对照
          • 12.1 五个全链路总览
          • 12.2 各平台优化优先级
          • 12.3 反直觉问题答疑(汇总)
        • 13.治理一层响应感知
          • 13.1 点击立即视觉反馈
          • 13.2 乐观更新(先显示再校验)
          • 13.3 骨架屏 + 占位
          • 13.4 加载态分级
          • 13.5 响应感知检查清单
        • 14.治理二层合成器
          • 14.1 transform / opacity 替代 left/top/width/height
          • 14.2 will-change / setLayerType 按需启用
          • 14.3 rAF 驱动动画
          • 14.4 60fps → 120fps 适配
          • 14.5 合成器优化检查清单
        • 15.治理三层业务异步
          • 15.1 业务逻辑完全异步
          • 15.2 避免动画期间触发 layout
          • 15.3 长任务分片(避免单帧 > 16ms)
          • 15.4 strict mode + 主线程 IO 拦截
          • 15.5 业务异步检查清单
        • 16.治理四层防抖批量
          • 16.1 搜索 / 输入 / 滚动防抖
          • 16.2 连续操作合并
          • 16.3 滚动监听内禁止重活
          • 16.4 动画期间暂停非关键任务
          • 16.5 ROI 排序
          • 16.6 避免反向收益
        • 17.求证实验 ⭐
          • 17.1 实验一:transform vs left/top
          • 17.2 实验二:rAF 时机决定卡顿率
          • 17.3 实验三:100ms 黄金线
          • 17.4 实验四:防抖与分片
          • 17.5 实验五:120Hz 高刷屏
          • 17.6 五大实验启示
        • 18.实战案例
          • 18.1 跨端同构案例:列表滚动卡顿
          • 18.2 平台特异案例:iOS 转场动画掉帧
          • 18.3 反例案例:will-change 滥用
        • 19.防劣化体系
          • 19.1 三道防线总览
          • 19.2 编码期 Lint
          • 19.3 CI 卡口
          • 19.4 线上 SLO
          • 19.5 文化建设
        • 20.跨平台速查
          • 20.1 工具速查
          • 20.2 关键 API 速查
          • 20.3 各平台动画优化清单
        • 21.总结与延伸
          • 21.1 五条核心原则
          • 21.2 五个常见误区
          • 21.3 一句话总结
          • 21.4 延伸阅读
          • 21.5 给团队的建议
    • 业务专项篇

    • 交付防御篇

  • 程序编程原理

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

  • 专栏
  • 性能优化实践
  • 流水线专项
杨充
2026-05-27
目录

动画交互响应优化

# 动画与交互响应优化

本文核心命题:动画与交互响应是"用户感受性能的最强放大镜"——指标好不代表体验好,但动画卡、交互延迟一定会被用户立刻感知。关键不是动画快,是首次反馈快——Doherty 100ms 是用户感知断崖,前 100ms 没反馈就是"卡"。


# 01.阅读说明

  • 本文卷归属:卷三 · 流水线篇 · 第 6 篇
  • 本文目标层级:L2 进阶 → L3 专家
  • 适用平台:Android(主) / iOS / Web / 跨端框架 / 嵌入式
  • 前置阅读:
    • 卷三·01 渲染管线与原理(动画本质是连续帧渲染)
    • 卷三·03 卡顿捕获与归因(交互延迟是卡顿的特殊场景)
  • 本文核心命题:

    动画 = 反馈快 + 帧顺滑 + 业务异步。
    输入响应 ≤ 100ms(Doherty 黄金线)+ 动画 60fps(每帧 ≤ 16.67ms)+ 业务逻辑不阻塞主线程 = 动画与交互的"三件套"。

全文 21 章地图:

   §01 阅读说明           §02 贯穿案例           §03 用户感知本质     §04 交互链路原理
   §05 度量与采集         §06 归因决策树
   §07 输入链路全链路 ⭐  §08 动画驱动全链路 ⭐   §09 合成器全链路 ⭐
   §10 高频输入全链路 ⭐  §11 跨端动画对照 ⭐    §12 跨端对照
   §13 治理一层响应感知 ⭐  §14 治理二层合成器 ⭐  §15 治理三层业务异步 ⭐  §16 治理四层防抖批量 ⭐
   §17 求证实验 ⭐         §18 实战案例           §19 防劣化体系          §20 跨平台速查
   §21 总结与延伸
1
2
3
4
5
6
7

阅读建议:先读 §02 案例 → §03/§04 拿到原理(含 Doherty 100ms 黄金线)→ §05/§06 学会度量归因 → §07-§11 五个全链路(输入/动画/合成/高频/跨端)→ §13-§16 四层治理 → §17 求证 → §18-§20 工程闭环。


# 02.贯穿案例

本案例贯穿全文:§03 看懂用户感知基线、§06 用决策树定位、§17 用实验复盘、§13-§16 给出分层闭环。

# 2.1 案例背景

某头部电商 V11.2 上线"加购物车飞入动画"(产品要求"加购更有反馈感"),灰度后用户反馈崩塌:

  • 加购按钮点击响应 P95 = 380ms(之前 60ms),用户大量"点了没反应"投诉。
  • 加购动画 FPS 平均 55 但 P99 帧时长 180ms(明显抖动)。
  • 加购转化率下降 8%——一次电商核心 KPI 的直接打击。
  • 用户主观调研"App 变慢了"打分从 4.3 降到 3.2。

研发组初步反应:"动画肯定要消耗资源啊,加购功能没变慢。"——这是典型的"功能 vs 体验"误区。

# 2.2 经验派的 3 周折腾(典型反面教材)

周次 动作 结果
第 1 周 把动画改用 JS setTimeout 驱动(怀疑 CSS 重) 丢帧率 18%(vsync 不对齐),更糟
第 2 周 把动画时长从 500ms 缩短到 300ms(怀疑动画太长) 用户感觉"突兀",仍说"卡"
第 3 周 给所有 view 加 will-change(怕合成器没启动) 显存涨 80MB,低端机 OOM

复盘:三周折腾错在"症状追逐"——动画体验问题永远是输入响应 + 动画流畅度双维度,不是单一指标。指标都达标了但用户还说卡,一定是这一章没做好。

# 2.3 方法派的 5 天闭环

Day 1(§06 输入延迟分段归因):

测量加购按钮 4 段:

  • T_input → T_dispatch = 8ms(正常)
  • T_dispatch → T_layout = 280ms(异常!业务逻辑过重)
  • T_layout → T_compose = 50ms(正常)
  • T_compose → T_display = 42ms(正常)

→ 元凶在"业务逻辑过重"——加购代码里同步走了"检查库存→更新购物车→上报埋点→刷新购物车数字"4 步同步。

Day 2(§06 动画归因 + §17 transform 实验):

动画 P99=180ms 的原因:

  • 飞入动画用 left/top(不走合成器)。
  • 同时触发购物车数字 setState(强制同步布局)。

Day 3-4(§13-§16 分层策略):

  • 第 1 层(响应感知):点击立即给视觉反馈(按钮按下态 + 飞入动画占位),业务逻辑异步。
  • 第 2 层(动画合成):left/top → transform/opacity,走合成器。
  • 第 3 层(业务异步):库存检查/上报埋点/数字更新都改异步,按钮立刻可点。
  • 第 4 层(防抖批量):连续加购合并触发,避免每次都全链路。

Day 5(上线 + 灰度验证):

# 2.4 上线效果

指标 经验派 3 周后 方法派 5 天后
加购点击响应 P95 380 ms 45 ms
动画 P99 帧时长 180 ms 17 ms
用户主观"流畅度" 3.2 4.5
加购转化率 -8% +3%(恢复并提升)
中低端机 OOM +5% 持平

核心洞察:动画与交互问题永远是输入响应 + 动画流畅度双维度。Doherty 阈值 100ms 是用户感知断崖(§17.3)——即使后续动画再流畅,前 100ms 没反馈就是"卡"。关键不是动画快,是首次反馈快。

# 2.5 案例如何串起本文

  • §03 用户感知基线 ▶▶ 100ms 黄金线是案例 380ms 翻车的物理基线。
  • §04 交互链路 + 4 段归因 ▶▶ Day 1 精准定位"T_dispatch → T_layout 280ms"。
  • §07-§11 五大全链路 ▶▶ 输入/动画/合成/高频/跨端 五条链路对应案例每一类问题。
  • §17 求证实验 ▶▶ §17.1 transform + §17.3 100ms + §17.4 防抖 + §17.5 长任务分片都在案例中变现。
  • §13-§16 四层治理 ▶▶ "响应感知→动画合成→业务异步→防抖批量"四层正是案例落地路径。

探索性思考:为什么"动画与交互"是性能优化中最被低估的领域?因为指标都"对"——FPS 60、延迟 < 100ms 都达标了。但用户依然投诉。指标的局限在于它"测量了某些维度" —— 但用户感受是多维度叠加 + 时序敏感的。好的工程师必须从"指标驱动"升级到"体验驱动"。


# 03.用户感知本质

# 3.1 一句话定义

动画与交互的本质是"用户感知的时间线" —— 用户从触发到看到反馈的时间序列,决定了"流畅"还是"卡"。

这句话隐含三个不可商量的物理约束:

约束一:用户感知的时间分级(基线)

时间 用户感受
< 16ms 即时(一帧)
16-100ms 流畅
100-300ms 可感知延迟
300ms-1s 明显卡顿
> 1s 中断感(Doherty 阈值)

Doherty 阈值(100ms):低于 100ms 用户感知"无延迟";超过 100ms 急剧下降。

约束二:动画卡顿的三种形态

   ┌──────────────────────────────────────────────┐
   │ ① 抖动(jitter)                              │
   │    FPS 平均够,但帧间方差大                    │
   │    (一会 60、一会 30)                         │
   │    比纯 30fps 更刺眼                          │
   ├──────────────────────────────────────────────┤
   │ ② 撕裂(tearing)                             │
   │    CPU 提交速度跟不上 GPU 合成                 │
   │    半屏新帧半屏旧帧                            │
   ├──────────────────────────────────────────────┤
   │ ③ 延迟(lag)                                 │
   │    动画总时长超过预期                          │
   │    比如 300ms 的过场实际跑了 600ms              │
   └──────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14

约束三:感知是"最差那帧"决定的

用户的卡顿感不是平均——而是被"最差那帧"放大。P99 帧时长比平均 FPS 重要 10×。

# 3.2 现象与代价

动画与交互问题的用户感知:

  • 点击没反馈:按下后 > 100ms 仍无视觉变化
  • 动画抖动:60fps 均值但偶发 100ms 帧
  • 滑动卡:列表滑动时 FPS 降到 30 以下
  • 页面切换迟钝:转场动画完成后还有"空白期"
  • 键盘弹出顿:输入弹起瞬间画面卡 1-2 帧

业务代价(行业实测数据):

  • 加购按钮响应每多 100ms,转化率 -1%
  • 列表滚动 FPS < 50 时留存 -2%
  • 动画抖动每 +1%(P99 帧时长 > 50ms 比例),主观评分 -0.3 分

▶▶ 回扣 §02 案例:加购响应 380ms 直接踩入"明显卡顿"区——Doherty 100ms 是物理感知断崖,不是工程指标。

# 3.3 度量准则与基准

资源视角(USE):

指标 含义 阈值参考
动画 FPS P95 动画期间帧率 95% 分位 ≥ 55(60Hz)
动画 P99 帧时长 长尾帧时长 < 25ms
GPU 利用率 合成压力 < 80%

请求视角(RED):

指标 含义 阈值参考
输入响应延迟 触摸到首帧反馈 P95 < 100ms
动画启动延迟 trigger 到首帧 < 50ms
动画完成时长偏差 实际 vs 预期 < 10%

行业基准:

指标 优秀 合格 不合格
输入响应 P95 < 50ms < 100ms > 200ms
动画 FPS P95(60Hz) > 58 > 55 < 50
动画 FPS P95(120Hz) > 115 > 110 < 100
强制同步布局/帧 0 < 1 ≥ 1

# 3.4 反直觉问题清单

带着这些问题阅读:

  1. 60fps 不卡,为什么用户还说"动画不顺滑"?
  2. 输入响应 < 100ms 为什么还是被嫌弃慢?
  3. 为什么"先 setState 再 layout"和"先 layout 再 setState"性能差 5 倍?
  4. CSS transform 比 left/top 快,但快在哪?
  5. 为什么 60fps 的动画在 120Hz 屏上反而看着不流畅?
  6. requestAnimationFrame 的回调时机为什么是"渲染前"而不是"渲染后"?
  7. 触控反馈的"100ms 黄金线"从哪来?
  8. 为什么动画使用 GPU 不是越多越好?

探索性思考:为什么 Doherty 100ms 是"物理感知"而非"工程经验"?因为人眼的视觉暂留 + 神经反应时间共约 100ms。这是生物学常数,不是工程参数 —— 用户不会因为你说"我们 200ms 也算流畅"就改变。好的产品设计必须尊重生物学 —— 工程指标可以妥协,物理约束不能。


# 04.交互链路原理

# 4.1 交互延迟分段模型

任何用户操作的延迟都可拆为 4 段:

T_input → T_dispatch → T_layout → T_compose → T_display
   ↑          ↑           ↑          ↑           ↑
  事件      分发到目标    布局重算    GPU 合成    屏幕显示
1
2
3

每一段都可独立度量:

  • T_input → T_dispatch:事件队列拥堵(主线程被阻塞)
  • T_dispatch → T_layout:业务逻辑过重(setState 链/同步网络)
  • T_layout → T_compose:layout/paint 抖动(强制同步布局、过度重绘)
  • T_compose → T_display:GPU 合成压力(层数太多、图层尺寸大)

# 4.2 三个共识

  • 平均 FPS 没用:用户感受的是"最差那帧",监控必须看 P99
  • 120Hz 不是简单 2 倍 60fps:每帧时间预算 8.3ms,对 CPU/GPU 是降维打击
  • GPU 不是免费的:开太多硬件加速层反而触发"层爆炸",性能更差

# 4.3 跨平台同构原理

所有平台都暴露"帧回调"和"输入事件"两套 API,只是名字不同。

跨平台术语对照

通用术语 Android iOS Web 嵌入式
帧回调 Choreographer CADisplayLink requestAnimationFrame 显示控制器
输入事件 MotionEvent UIEvent PointerEvent 中断
合成器 RenderThread + SurfaceFlinger Core Animation Compositor varies
硬件加速 setLayerType shouldRasterize will-change varies
动画驱动 ValueAnimator / Choreographer CABasicAnimation CSS / rAF 自定义

优化方法(合成器加速、避免主线程阻塞、layer 化)跨平台通用。

# 4.4 平台差异点矩阵

维度 Android iOS Web 跨端框架
动画驱动 ValueAnimator + Choreographer CABasicAnimation + Render Server CSS Transition / rAF 框架内置
合成层创建 setLayerType implicit / shouldRasterize will-change 框架决定
输入响应链路 InputDispatcher → ViewRoot UIKit → RunLoop 浏览器 → JS 桥接层
高刷支持 ARR (12+) ProMotion 浏览器自适配 varies

探索性思考:为什么"输入延迟分段"是工程上的稳定划分?因为它对应了硬件 → OS → 应用 → GPU → 屏幕 的物理链路。好的诊断模型一定对应物理结构 —— 你不能跳过任何一段,每一段都要单独度量。全链路才能精准归因。


# 05.度量与采集

# 5.1 三类采集方案

   ① 帧绘制时间序列(fps + 帧时长)
   ② 输入到首帧延迟(4 段链路)
   ③ 人眼级视频对比(地面真值)
1
2
3

① 帧绘制时间序列

// Android
Choreographer.getInstance().postFrameCallback(object : FrameCallback {
    var lastTime = 0L
    override fun doFrame(frameTimeNanos: Long) {
        val interval = (frameTimeNanos - lastTime) / 1_000_000
        recordFrame(interval)
        lastTime = frameTimeNanos
        Choreographer.getInstance().postFrameCallback(this)
    }
})
1
2
3
4
5
6
7
8
9
10
// Web
function frame(timestamp) {
    recordFrame(timestamp);
    requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
1
2
3
4
5
6

② 输入到首帧延迟

// Android:在 ViewRoot / Choreographer 上打点
button.setOnClickListener {
    val tInput = SystemClock.elapsedRealtimeNanos()
    Choreographer.getInstance().postFrameCallback {
        val tDisplay = SystemClock.elapsedRealtimeNanos()
        report("input_to_first_frame", (tDisplay - tInput) / 1_000_000)
    }
}
1
2
3
4
5
6
7
8

③ 人眼级视频对比

  • 用 240fps 高速摄像录制屏幕
  • OpenCV 自动比对帧差
  • 仅用于关键评估(不可线上)

# 5.2 各方案的可见盲区

方案 钩子位置 数据粒度 性能开销 跨端通用性 线上可用 主要局限
① 帧时序 帧回调 单帧 极低 跨端 是 不知 GPU
② 输入延迟 输入分发 全链路 低 跨端 是 工程难度大
③ 视频对比 屏幕外 物理像素 高(设备成本) 全 否 仅线下

# 5.3 跨平台采集对照表

平台 帧回调 输入回调 高速摄像
Android Choreographer MotionEvent 通用
iOS CADisplayLink UIEvent 通用
前端 rAF PointerEvent 通用
跨端框架 框架内置 框架转发 通用

# 5.4 数据可信度评估

  • 帧时序:可信度高,但只看应用端。
  • 输入延迟:可信度高,覆盖全链路。
  • 视频对比:可信度最高(地面真值),成本最高。

探索性思考:为什么"输入到首帧"是动画度量的"金标准"?因为它直接对应用户感知——用户按下到看到反馈的物理时间。所有其他指标(FPS、卡顿率)都是间接的代理 —— 输入到首帧才是用户的真实体验。好的指标设计要"贴近用户视角"。


# 06.归因决策树

# 6.1 动画卡顿决策树

   动画不顺滑
      │
      ├─ 平均 FPS 达标吗?
      │     │
      │     ├─ 否 → 整体性能问题(参考渲染篇)
      │     └─ 是 → 帧间方差大(抖动)
      │           ├─ 主线程被偶发任务打断(GC / IO / Layout)
      │           └─ GPU 提交不稳定(层频繁 invalidate)
      │
      └─ 是动画本身慢还是输入响应慢?
            ├─ 动画 → 检查 transform/opacity 是否走合成器
            └─ 输入 → 检查事件分发链路、首帧绘制
1
2
3
4
5
6
7
8
9
10
11
12

▶▶ 回扣 §02 案例:决策树两条路径同时走——动画本身用 left/top(不走合成器)+ 输入响应被业务逻辑阻塞 280ms。两个维度任一失守体验都崩。

# 6.2 交互延迟分段归因

按 §04.1 四段模型逐段排查:

T_input → T_dispatch 延迟:

  • 主线程被阻塞(GC / IO / 长任务)
  • 输入队列拥堵
  • → 治理:长任务切片,主线程减负

T_dispatch → T_layout 延迟:

  • 业务逻辑同步执行(网络/DB/计算)
  • setState 链路过长
  • → 治理:业务异步化,乐观更新

T_layout → T_compose 延迟:

  • 强制同步布局(写后立即读)
  • 过度重绘
  • → 治理:分离读写,减节点

T_compose → T_display 延迟:

  • GPU 合成压力大
  • 层爆炸
  • → 治理:减少合成层

# 6.3 强制同步布局检测

典型反模式:

// ❌ 写完属性立刻读属性 → 强制 layout
element.style.width = '100px';
const height = element.offsetHeight;  // 强制 layout
element.style.height = (height + 10) + 'px';

// ✅ 分离读写
const heights = elements.map(el => el.offsetHeight);  // 批量读
elements.forEach((el, i) => el.style.height = (heights[i] + 10) + 'px');  // 批量写
1
2
3
4
5
6
7
8

检测方法:

  • Chrome DevTools Performance 紫色块("Layout")
  • Android Systrace measure/layout 高频出现
  • iOS Instruments layoutSubviews 调用栈

# 6.4 GPU 合成压力归因

典型场景:

  • will-change 全量启用 → 层爆炸
  • 多个半透明层叠加 → overdraw
  • 动画区域大(全屏移动) → 整层重绘
  • 大尺寸 layer(如全屏视频) → 显存压力

探索性思考:为什么"决策树"是动画归因最有效的工具?因为动画问题的症状-根因映射相对结构化 —— 抖动就是看主线程,输入慢就是看分发链路,每个维度独立诊断。结构化的领域适合用决策树 —— 动画归因不需要 ML。


# 07.输入链路全链路

输入链路是用户感知"反馈快"的物理基础。理解从触摸到首帧的完整链路,才能精准优化。

# 7.1 Android 输入链路全链路

   ① 触摸屏硬件中断
      ↓ ~5ms
   ② InputReader(独立线程读 /dev/input)
      ↓
   ③ InputDispatcher 分发到目标窗口
      ↓ Binder
   ④ 应用 Choreographer 主线程接收
      ↓
   ⑤ ViewRootImpl.dispatchInputEvent
      ↓
   ⑥ View.dispatchTouchEvent → onClickListener
      ↓
   ⑦ 业务代码处理(理想 < 50ms)
      ↓
   ⑧ View.invalidate / setState
      ↓
   ⑨ 下一个 Vsync → measure/layout/draw
      ↓
   ⑩ RenderThread → GPU
      ↓
   ⑪ SurfaceFlinger 合成上屏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

关键瓶颈:

  • ⑦ 业务代码(最常超时的环节)
  • ⑨ measure/layout(强制同步布局)
  • ⑩ GPU 合成(层爆炸)

优化路径:

  • 业务代码异步化
  • 立即视觉反馈(在 ⑦ 阶段直接修改 View 属性)
  • 合成器加速

# 7.2 iOS 输入链路全链路

   ① 触摸屏硬件 → IOKit
      ↓
   ② SpringBoard 路由到目标 App
      ↓
   ③ 应用 RunLoop 接收
      ↓
   ④ UIApplication.sendEvent
      ↓
   ⑤ UIView.touchesBegan / target-action
      ↓
   ⑥ 业务代码处理
      ↓
   ⑦ setNeedsDisplay / setNeedsLayout
      ↓
   ⑧ Render Server 合成上屏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

iOS 关键约束:

  • Watchdog 监控(操作 8s 限制)
  • 主线程职责:UIKit 操作必须主线程
  • Core Animation 隐式动画也在这个链路

# 7.3 Web 输入链路全链路

   ① 浏览器进程接收触摸/鼠标事件
      ↓
   ② 渲染进程主线程 JS
      ↓
   ③ pointerdown/touchstart 事件
      ↓
   ④ 业务 JS handler
      ↓
   ⑤ DOM 修改 / setState
      ↓
   ⑥ Style / Layout / Paint
      ↓
   ⑦ Compositor 合成
      ↓
   ⑧ 屏幕扫描
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Web 特殊性:

  • 主线程是 JS + DOM 共享
  • passive event listener 提示浏览器"不会 preventDefault",让滚动可以并行
  • INP(Interaction to Next Paint)= 输入到下一帧

# 7.4 输入响应优化路径

   ┌──────────────────────────────────────┐
   │ 立即视觉反馈(< 16ms)                  │
   │   按下态 / Material Ripple / 加载占位  │
   │   不需要等业务,直接动画属性             │
   ├──────────────────────────────────────┤
   │ 业务异步(不阻塞主线程)                 │
   │   网络 / DB / 计算 全部异步              │
   │   主线程仅做 UI 更新                    │
   ├──────────────────────────────────────┤
   │ 乐观更新(先 UI 后校验)                │
   │   点赞 → 立即 +1 → 后台请求             │
   │   失败回滚                              │
   └──────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

# 7.5 跨端输入响应性能数据

平台 硬件中断到 JS handler 业务处理预算 总预算
Android(高端机) ~10ms < 50ms < 100ms
Android(低端机) ~20ms < 80ms < 200ms
iOS ~10ms < 50ms < 100ms
Web ~10ms < 50ms < 100ms(INP 标准)

▶▶ 回扣 §02 案例:T_dispatch → T_layout 280ms 是案例的"凶手"——业务代码同步走了"检查库存→更新购物车→上报埋点→刷新数字"4 步。业务代码必须异步化是输入响应优化的"第一原则"。

探索性思考:为什么"立即视觉反馈"比"业务做完反馈"更重要?因为人眼对变化最敏感——只要按钮变了状态,用户就感觉"按到了",即使后台还在处理。视觉反馈 ≠ 业务完成 —— 这是用户体验设计的核心智慧。


# 08.动画驱动全链路

# 8.1 Android 动画驱动全链路

   ① ValueAnimator.start()
      ↓
   ② Choreographer.postFrameCallback
      ↓ 每个 Vsync
   ③ doFrame() → ValueAnimator.animateValue()
      ↓
   ④ 计算当前动画值(基于 fraction)
      ↓
   ⑤ 触发 listener 更新 View 属性
      ↓
   ⑥ View.invalidate
      ↓
   ⑦ Choreographer 调度 measure/layout/draw
      ↓
   ⑧ RenderThread + GPU
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

关键点:

  • Choreographer 严格 Vsync 对齐
  • ValueAnimator 是 ObjectAnimator 的基础
  • ViewPropertyAnimator 直接走 RenderThread(更快)
// 反例:ObjectAnimator + 普通属性
ObjectAnimator.ofFloat(view, "translationX", 0f, 100f).start()

// 正例:ViewPropertyAnimator(走 RenderThread)
view.animate().translationX(100f).setDuration(300).start()
1
2
3
4
5

# 8.2 iOS Core Animation 全链路

   ① layer.animateKeyPath / UIView.animate
      ↓
   ② Core Animation 创建 CABasicAnimation
      ↓
   ③ Animation tree 构建
      ↓
   ④ 提交到 Render Server(独立进程)
      ↓
   ⑤ Render Server 每帧插值计算
      ↓
   ⑥ 应用到 layer(无需主线程参与)
1
2
3
4
5
6
7
8
9
10
11

iOS 优势:

  • Core Animation 在 Render Server,不占主线程
  • transform / opacity 动画完全 GPU
  • 隐式动画(CALayer 属性变化自动动画)

注意点:

  • 显式动画完成 callback 在主线程
  • layoutIfNeeded 会破坏动画(需动画前确保布局稳定)

# 8.3 Web 动画驱动全链路

CSS Transition / Animation:

   ① 改 CSS 属性
      ↓
   ② Compositor 接管(如果是 transform/opacity)
      ↓
   ③ 每帧插值
      ↓
   ④ 直接合成
1
2
3
4
5
6
7

JS rAF:

   ① requestAnimationFrame(frame)
      ↓ 每个 Vsync
   ② frame 回调 → 计算动画值
      ↓
   ③ 修改 DOM 属性
      ↓
   ④ Style / Layout / Paint / Composite
1
2
3
4
5
6
7

Web Animation API:

element.animate(
    [{ transform: 'translateX(0)' }, { transform: 'translateX(100px)' }],
    { duration: 300, easing: 'ease-out' }
).onfinish = () => { /* ... */ };
1
2
3
4

# 8.4 高刷适配(60Hz → 120Hz)

   60Hz:每帧预算 16.67ms
   120Hz:每帧预算 8.33ms
1
2

适配要点:

  • 每帧任务必须 ≤ 5ms(留 3ms buffer)
  • 动态读 refreshRate(不要硬编码 16.67ms)
  • 静态画面降回低帧率省电(ARR / ProMotion)

# 8.5 跨端动画驱动对照

平台 默认驱动 推荐 高刷支持
Android Choreographer ViewPropertyAnimator ARR (12+)
iOS RunLoop + Core Animation UIView.animate ProMotion
Web requestAnimationFrame CSS / Web Animation API 浏览器自适配
Compose Recomposition + animateXxx Modifier.graphicsLayer ARR
Flutter Ticker AnimationController 60/120fps

▶▶ 回扣 §02 案例:方法派 Day 4 第 2 层"left/top → transform/opacity"是动画从抖动 P99=180ms → 17ms 的关键——走合成器后主线程完全不参与动画的每帧计算。

探索性思考:为什么"合成器 + 独立线程/进程"是现代动画框架的共同设计?因为它解耦了"应用代码"和"动画驱动"——应用代码可以慢,动画依然 60fps。好的架构通过"解耦"换"鲁棒性"。


# 09.合成器全链路

合成器(Compositor)是动画顺滑的物理基础。理解合成器的工作原理,才能精准优化。

# 9.1 合成器物理原理

   传统渲染(无合成器):
   每帧 → CPU 计算 → CPU 绘制 → GPU 上传 → 显示
   主线程参与每一步
   
   有合成器:
   每帧 → CPU 仅传递变换矩阵 → GPU 合成 layer 纹理 → 显示
   主线程几乎不参与
1
2
3
4
5
6
7

关键认知:合成器把"光栅化"和"合成"分开——光栅化只在 layer 内容变化时做,合成每帧做但开销小。

# 9.2 哪些操作走合成器

   ┌────────────────────────────────────┐
   │ 走合成器(GPU only,60fps 物理保证)│
   │   transform: translate / rotate /   │
   │              scale / matrix          │
   │   opacity                            │
   │   filter(部分)                     │
   ├────────────────────────────────────┤
   │ 不走合成器(必须走主线程)            │
   │   left / top / right / bottom        │
   │   width / height                     │
   │   margin / padding                   │
   │   color / background                 │
   │   font-size / font-weight            │
   └────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 9.3 layer 化机制

Web will-change:

.box { will-change: transform; }
1

Android setLayerType:

view.setLayerType(View.LAYER_TYPE_HARDWARE, null)
1

iOS shouldRasterize:

layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
1
2

作用:提前把 view 提升为独立 layer,避免动画启动瞬间的 layer 化卡顿。

代价:

  • 每个 layer 占独立显存
  • 全开会触发"层爆炸"(§02 案例第 3 周翻车)
  • 仅对真正会动画的元素启用

# 9.4 层爆炸的真实代价

   理想:3-5 个动画层
   合理:< 20 个层
   危险:50+ 层(GPU 内存压力)
   爆炸:100+ 层(中端机 OOM)
1
2
3
4

§02 案例第 3 周显存涨 80MB,低端机 OOM 就是层爆炸的真实代价。

# 9.5 跨端合成器对照

平台 合成器 提示 API 限制
Android RenderThread + SurfaceFlinger setLayerType API 11+
iOS Core Animation Render Server shouldRasterize 始终可用
Web Compositor 线程 will-change 现代浏览器
Flutter Skia 合成 RepaintBoundary 框架管理

▶▶ 回扣 §02 案例:方法派 Day 4 第 2 层只对"飞入动画"元素启用 will-change,没有滥用——这是经验派"全开 will-change"翻车的反面教材。合成器是好工具,但要精准使用。

探索性思考:为什么"合成器加速"是动画领域最重要的发明?因为它把"动画 = 主线程负担"的等式打破了——transform 动画零主线程参与。这是渲染架构的"分而治之" —— CPU 做静态,GPU 做动态,每个核心做自己擅长的事。


# 10.高频输入全链路

滚动、拖动、输入框打字等高频输入是动画性能的"重灾区"。理解高频输入的全链路才能精准优化。

# 10.1 滚动事件全链路

   ① 用户滑动
      ↓ 每个 Vsync ~16.67ms
   ② 触发 scroll 事件
      ↓
   ③ JS handler 执行
      ↓
   ④ 业务计算(getBoundingClientRect 等)
      ↓
   ⑤ 修改 DOM / 触发动画
      ↓
   ⑥ 渲染管线
1
2
3
4
5
6
7
8
9
10
11

关键瓶颈:

  • 滚动期间每帧都触发 scroll handler
  • handler 内任何"读 + 写"组合都强制同步布局
  • 60fps 滚动 = 每秒 60 次 handler 调用

典型反模式:

// ❌ scroll handler 内强制同步布局
window.addEventListener('scroll', () => {
    const top = element.getBoundingClientRect().top;  // 强制 layout
    if (top < 0) doSomething();
});

// ✅ 用 IntersectionObserver
const observer = new IntersectionObserver(entries => { /* ... */ });
observer.observe(element);
1
2
3
4
5
6
7
8
9

# 10.2 输入框打字全链路

   ① 用户键入字符
      ↓
   ② input/keyup 事件
      ↓
   ③ JS handler(搜索/校验等)
      ↓
   ④ 网络请求 / DOM 更新
      ↓
   ⑤ 渲染
1
2
3
4
5
6
7
8
9

典型反模式:

// ❌ 每键入一次就搜索
input.addEventListener('input', e => {
    api.search(e.target.value);  // 100 字符 = 100 次请求
});

// ✅ 防抖
const debouncedSearch = debounce(api.search, 300);
input.addEventListener('input', e => debouncedSearch(e.target.value));
1
2
3
4
5
6
7
8

# 10.3 拖动 / 缩放手势全链路

   ① touch start
      ↓
   ② 持续 touchmove(每帧)
      ↓ 每 16.67ms
   ③ 业务计算(位置 / 缩放比例)
      ↓
   ④ DOM 更新 / 动画
      ↓
   ⑤ 渲染
1
2
3
4
5
6
7
8
9

优化手段:

  • 用 transform(避免触发 layout)
  • 长任务分片
  • passive event listener(让浏览器并行滚动)

# 10.4 高频输入优化策略

   场景 → 优化策略:
   ├─ 搜索 → 300-500ms 防抖
   ├─ 滚动 → 16ms throttle 或 IntersectionObserver
   ├─ 拖动 → transform + rAF
   ├─ 输入框 → 防抖 + 长任务分片
   └─ 点赞连击 → 100ms 合并
1
2
3
4
5
6

# 10.5 跨端高频输入对照

场景 Android iOS Web
滚动 RecyclerView 内置优化 UICollectionView IntersectionObserver
拖动 GestureDetector UIPanGestureRecognizer pointerdown + transform
防抖 自实现 自实现 lodash.debounce
Throttle 自实现 自实现 lodash.throttle

▶▶ 回扣 §02 案例:方法派 Day 4 第 4 层"连续加购合并触发"就是高频输入合并的典型——避免每次加购都全链路。

探索性思考:为什么"防抖 + Throttle"是高频输入的"必修课"?因为它们解决了"事件频率 > 处理能力"的物理矛盾。没有防抖的代码 = 让 CPU 做无用功 —— 用户键入 100 字符,搜索 99 次都被覆盖,只有最后一次有效。


# 11.跨端动画对照

# 11.1 各平台动画体系

Android:

  • View 体系:ValueAnimator / ObjectAnimator / ViewPropertyAnimator
  • Compose:animateXxxAsState / Modifier.animateXxx
  • 物理动画:SpringAnimation / FlingAnimation

iOS:

  • UIKit:UIView.animate / UIViewPropertyAnimator
  • Core Animation:CABasicAnimation / CAKeyframeAnimation
  • SwiftUI:withAnimation / animation(.easeInOut)

Web:

  • CSS:transition / @keyframes
  • JS:Web Animation API / rAF
  • 框架:React Spring / Framer Motion

跨端框架:

  • Flutter:AnimationController + Tween
  • React Native:Animated API
  • Compose:Compose Animation

# 11.2 跨端动画性能对照

平台 60fps 难度 120fps 难度 物理动画
Android Native 中 中-高 内置(DynamicAnimation)
iOS Native 低 低(ProMotion) 内置(UIDynamicAnimator)
Web 中 中 第三方库
Flutter 低(Skia) 中 内置
React Native 中(Bridge) 难 Animated.spring

# 11.3 各平台最佳实践

Android:

  • 优先 ViewPropertyAnimator(走 RenderThread)
  • 用 transform 替代 left/top
  • Compose 用 animateXxxAsState
  • 高刷适配(ARR)

iOS:

  • 优先 UIView.animate(走 Render Server)
  • 用 transform 替代 frame
  • 复杂动画用 CABasicAnimation
  • ProMotion 适配

Web:

  • 优先 CSS Transition(走 Compositor)
  • 用 transform/opacity 替代 left/top
  • will-change 仅在动画前启用
  • INP 监控

# 11.4 反直觉问题答疑

问题 答案
transform 比 left/top 快多少? FPS 32 → 60,主线程 95% → 12%
rAF vs setTimeout 差多少? 丢帧率 18% → 2%
100ms 黄金线从哪来? 视觉暂留 + 神经反应(Doherty 实验)
60fps 在 120Hz 屏卡吗? 不卡,但盲测胜率 78% 输给 120fps
will-change 全开就快吗? 错。层爆炸 OOM
GPU 越多越好吗? 错。合成层超 50 个就开始劣化

探索性思考:为什么"跨端动画方法论高度统一"?因为底层都是 GPU 合成 + Vsync 驱动。应用层 API 千差万别,但物理本质是同一个。理解物理层的工程师,看任何平台都是"换 API 名字而已"。


# 12.跨端对照

# 12.1 五个全链路总览

链路 Android iOS Web Flutter
输入链路 InputDispatcher → ViewRoot UIKit → RunLoop 浏览器 → JS 桥接层
动画驱动 Choreographer + ViewPropertyAnimator Core Animation Render Server rAF + CSS Ticker + Animation
合成器 RenderThread + SurfaceFlinger Render Server Compositor 线程 Skia
高频输入 RecyclerView / GestureDetector UICollectionView IntersectionObserver 内置
跨端动画 View / Compose UIKit / SwiftUI CSS / WAA Animated

# 12.2 各平台优化优先级

Android:

  1. transform 替代 left/top
  2. ViewPropertyAnimator 替代 ObjectAnimator
  3. 业务逻辑完全异步
  4. 滚动监听内禁重活
  5. 高刷适配

iOS:

  1. transform 替代 frame
  2. UIView.animate(避免手动 setNeedsLayout)
  3. 业务异步
  4. ProMotion 适配
  5. 避免 layoutIfNeeded 在动画期间

Web:

  1. transform/opacity 替代 left/top
  2. CSS Transition 替代 JS
  3. 防抖 + IntersectionObserver
  4. INP 监控
  5. 业务异步

# 12.3 反直觉问题答疑(汇总)

问题 答案
60fps 不卡为何用户说卡? P99 帧时长 + 输入延迟才是真相
输入 < 100ms 为何还嫌弃? 用户视觉暂留是 100ms,超过就被感知
setState 顺序差 5×? 强制同步布局会触发额外 layout
transform 快在哪? 走合成器,零主线程参与
60fps 在 120Hz 屏不流畅? 视觉对比 120fps 时显得抖
rAF 为何渲染前? 给主线程留时间准备
100ms 黄金线从哪? Doherty 实验 + 视觉神经常数
GPU 越多越好? 错。层爆炸反而劣化

▶▶ 回扣 §02 案例:经验派 100% 命中"反直觉问题"——把"动画快 = 体验好"当真理。真相是:体验 = 输入响应 + 动画流畅 双维度,前者决定 80% 感知。


# 13.治理一层响应感知

核心命题:§17.3 实验证明 100ms 是用户感知断崖。关键不是动画快,是首次反馈快。

# 13.1 点击立即视觉反馈

机理:按下态 / Material Ripple / 加载占位都是 < 16ms 的视觉反馈。

代码:

button.setOnClickListener { v ->
    v.alpha = 0.6f  // 立即视觉反馈(< 16ms)
    v.animate().alpha(1f).setDuration(100).start()
    // 业务逻辑异步
    lifecycleScope.launch { handleClickAsync() }
}
1
2
3
4
5
6
button.addEventListener('click', e => {
    e.target.classList.add('active');  // 立即视觉反馈
    setTimeout(() => e.target.classList.remove('active'), 100);
    // 业务异步
    handleClickAsync();
});
1
2
3
4
5
6

收益:§02 案例加购响应 380ms → 45ms。

边界:业务必须真的异步,不能在视觉反馈后又阻塞主线程。

# 13.2 乐观更新(先显示再校验)

机理:用户点赞 → 立即 +1 显示 → 后台请求;失败再回滚。

代码:

fun like(post: Post) {
    post.likes++  // 乐观更新
    updateUI()
    api.like(post.id).onFailure {
        post.likes--  // 失败回滚
        updateUI()
        toast("点赞失败")
    }
}
1
2
3
4
5
6
7
8
9

收益:用户感知响应 0ms。

边界:

  • 失败回滚需 UX 设计
  • 不能用于强一致性操作(如支付)
  • 需保证幂等性

# 13.3 骨架屏 + 占位

机理:加载期显示页面骨架,让用户看到"页面在准备"。

代码:

<div class="skeleton">
    <div class="skeleton-header"></div>
    <div class="skeleton-body"></div>
    <div class="skeleton-footer"></div>
</div>
1
2
3
4
5

收益:感知延迟 -60%(虽物理时长不变)。

边界:骨架屏不能过于复杂;与真实内容布局对齐。

# 13.4 加载态分级

   < 100ms:无任何提示(即时)
   100-1000ms:按下态 / 旋转图标
   1-10s:进度条 + 取消按钮
   > 10s:详细进度 + 重试机制
1
2
3
4

# 13.5 响应感知检查清单

  • [ ] 所有点击有视觉反馈(按下态 / Ripple)
  • [ ] 表单提交立即变 loading 态
  • [ ] 列表加载有骨架屏
  • [ ] 长任务有进度提示
  • [ ] 关键操作支持乐观更新

探索性思考:为什么"立即视觉反馈"是最容易做但最被忽视的优化?因为开发者本能想"先做完业务再反馈",但用户感知反过来——先看到反馈再等业务才是好体验。用户视角 vs 工程师视角是两种思维 —— 优秀工程师能切换。

▶▶ 回扣 §02 案例:方法派 Day 4 第 1 层"点击立即视觉反馈"是 380ms → 45ms 的关键——用户感知响应从"明显卡"变"丝滑",不需要业务真的快。


# 14.治理二层合成器

核心命题:§17.1 实验走合成器是 FPS 32 → 60 的根本。

# 14.1 transform / opacity 替代 left/top/width/height

代码:

/* 反例 */
.box { transition: left 0.3s; }
.box.active { left: 100px; }

/* 正例 */
.box { transition: transform 0.3s; }
.box.active { transform: translateX(100px); }
1
2
3
4
5
6
7
// Android 反例
view.x = 100f
ObjectAnimator.ofFloat(view, "x", 0f, 100f).start()

// 正例:translationX 走 RenderThread
view.translationX = 100f
view.animate().translationX(100f).setDuration(300).start()
1
2
3
4
5
6
7

收益:§17.1 实验FPS 32 → 60,主线程 CPU 95% → 12%。

边界:transform 只能做位移/缩放/旋转,改尺寸仍需 layout。

# 14.2 will-change / setLayerType 按需启用

机理:提前提升合成层,避免动画启动瞬间的 layer 化卡顿。

代码:

/* 仅对真正会动画的元素启用 */
.animating { will-change: transform; }
1
2
view.setLayerType(View.LAYER_TYPE_HARDWARE, null)
// 动画结束后释放
animation.doOnEnd {
    view.setLayerType(View.LAYER_TYPE_NONE, null)
}
1
2
3
4
5

收益:动画启动顺滑。

边界:§02 案例第 3 周翻车证明全开 will-change 会爆显存——仅对真正会动画的元素启用。

# 14.3 rAF 驱动动画

机理:§17.2 实验丢帧率 18% → 2%。

代码:

function animate() {
    update();
    requestAnimationFrame(animate);  // 严格 vsync 对齐
}
requestAnimationFrame(animate);
1
2
3
4
5
val choreographer = Choreographer.getInstance()
choreographer.postFrameCallback(object : FrameCallback {
    override fun doFrame(frameTimeNanos: Long) {
        update()
        choreographer.postFrameCallback(this)
    }
})
1
2
3
4
5
6
7

收益:vsync 对齐,无漂移。

边界:setTimeout 只用于业务定时器;动画必须用 rAF / Choreographer / CADisplayLink。

# 14.4 60fps → 120fps 适配

代码:见 卷三·02 §16.2 动态读刷新率。

val refreshRate = display.refreshRate
val frameBudgetMs = 1000f / refreshRate
1
2

收益:§17.5 实验78% 盲测胜率。

边界:未优化反而更糟,需配套(每帧 ≤ 5ms)。

# 14.5 合成器优化检查清单

  • [ ] 所有位移动画用 transform
  • [ ] 所有透明动画用 opacity
  • [ ] will-change 仅对动画元素
  • [ ] 动画用 rAF 驱动
  • [ ] 高刷设备做适配

探索性思考:为什么"合成器加速"是动画领域最大的范式转变?因为它把"动画 = 主线程负担"的等式打破了。在合成器之前,60fps 动画意味着主线程每秒 60 次 invalidate;之后,主线程几乎不参与动画。这是渲染架构的"分而治之"。

▶▶ 回扣 §02 案例:方法派 Day 4 第 2 层"left/top → transform/opacity"让动画走合成器——FPS 平均 55 → 60,P99 帧时长 180ms → 17ms。走合成器是物理决定的"丝滑保证"。


# 15.治理三层业务异步

核心命题:§02 案例T_dispatch → T_layout 280ms 是元凶——业务逻辑同步是最大的"反馈延迟来源"。

# 15.1 业务逻辑完全异步

代码:

button.setOnClickListener {
    // 立即视觉反馈
    showFeedback()
    // 业务逻辑切后台
    lifecycleScope.launch(Dispatchers.IO) {
        val result = doBusinessLogic()  // 网络/DB/计算
        withContext(Dispatchers.Main) { 
            updateUI(result) 
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11

收益:主线程不被阻塞。

边界:

  • 异步回调需检查 lifecycle 防泄漏
  • 用 viewModelScope / lifecycleScope 自动管理生命周期
  • 异步过程中用户可能离开页面,需考虑

# 15.2 避免动画期间触发 layout

机理:动画 + setState(改 layout 属性)= 强制重排,丢帧。

代码:

// 反例:动画期间频繁 setState
animateBox(() => {
    setState({ counter: counter + 1 });  // 触发 React reconciliation
});

// 正例:动画结束后再 setState
animateBox().then(() => setState({ counter: counter + 1 }));
1
2
3
4
5
6
7

收益:动画 P99 帧时长大幅下降。

边界:业务需对齐动画 + 数据更新时序。

# 15.3 长任务分片(避免单帧 > 16ms)

机理:见 卷三·05 §13.2 IdleHandler。

代码:

function processBatch(items, callback) {
    const deadline = performance.now() + 5;
    while (items.length && performance.now() < deadline) {
        process(items.shift());
    }
    if (items.length) {
        requestIdleCallback(() => processBatch(items, callback));
    } else {
        callback();
    }
}
1
2
3
4
5
6
7
8
9
10
11
Looper.myQueue().addIdleHandler {
    val deadline = SystemClock.uptimeMillis() + 5
    while (pendingTasks.isNotEmpty() && SystemClock.uptimeMillis() < deadline) {
        pendingTasks.poll().run()
    }
    pendingTasks.isNotEmpty()
}
1
2
3
4
5
6
7

收益:动画期间不被业务任务打断。

边界:分片要保留状态机一致性。

# 15.4 strict mode + 主线程 IO 拦截

代码:见 卷二·06 §13.1 StrictMode。

收益:开发期 100% 拦截主线程 IO。

# 15.5 业务异步检查清单

  • [ ] 所有点击/输入 handler 第一句是"显示反馈"
  • [ ] 所有 IO 操作走子线程
  • [ ] 所有计算 > 5ms 的任务走子线程
  • [ ] 动画期间不触发 setState 改 layout 属性
  • [ ] 长任务切片 < 5ms / 片

探索性思考:为什么"业务异步"是最难推动的优化?因为它涉及"代码组织"——开发者本能写"链式同步"代码(a().b().c())。异步代码可读性差 —— 这是工程上的真实代价。但 Kotlin 协程 / Swift async-await 的出现让异步代码"看起来同步",是巨大进步。

▶▶ 回扣 §02 案例:方法派 Day 4 第 3 层"业务异步"让 280ms 主线程占用 → 0ms——这是 380ms 响应 → 45ms 的根本原因。


# 16.治理四层防抖批量

核心命题:§17.4 实验证明高频输入必须配合防抖 + 长任务分片。

# 16.1 搜索 / 输入 / 滚动防抖

代码:

// 搜索:300ms 防抖
const debouncedSearch = debounce(search, 300);
input.addEventListener('input', e => debouncedSearch(e.target.value));

// 滚动:16ms throttle(保持 60fps)
const throttledScroll = throttle(handleScroll, 16);
window.addEventListener('scroll', throttledScroll);
1
2
3
4
5
6
7
// Kotlin Flow 防抖
val searchFlow = MutableSharedFlow<String>()
lifecycleScope.launch {
    searchFlow
        .debounce(300)
        .collect { search(it) }
}
1
2
3
4
5
6
7

收益:§17.4 实验FPS 18 → 58。

边界:防抖延迟用户感知;可视化"loading"提示缓解。

# 16.2 连续操作合并

代码:

// 连续点赞合并
val likeChannel = Channel<LikeEvent>(BUFFERED)
lifecycleScope.launch {
    likeChannel.consumeAsFlow()
        .debounce(100)  // 100ms 内合并
        .collect { api.batchLike(it) }
}
1
2
3
4
5
6
7

收益:服务端 QPS -90%;电池友好。

边界:合并间隔需用户感知接受。

# 16.3 滚动监听内禁止重活

代码:

// 反例:scroll handler 强制 layout
window.addEventListener('scroll', () => {
    const top = element.getBoundingClientRect().top;  // 强制 layout
    if (top < 0) doSomething();
});

// 正例:IntersectionObserver
const observer = new IntersectionObserver(entries => { 
    entries.forEach(entry => {
        if (entry.isIntersecting) doSomething();
    });
});
observer.observe(element);
1
2
3
4
5
6
7
8
9
10
11
12
13

收益:滚动 FPS 大幅提升。

边界:IntersectionObserver 兼容性需检查(IE 不支持)。

# 16.4 动画期间暂停非关键任务

代码:

recyclerView.addOnScrollListener(object : OnScrollListener() {
    override fun onScrollStateChanged(rv: RecyclerView, state: Int) {
        when (state) {
            SCROLL_STATE_DRAGGING -> {
                Glide.with(rv.context).pauseRequests()
                analyticsExecutor.pause()
            }
            SCROLL_STATE_IDLE -> {
                Glide.with(rv.context).resumeRequests()
                analyticsExecutor.resume()
            }
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

收益:动画/滚动期间 CPU 留给关键任务。

边界:暂停的任务在 IDLE 后要恢复。

# 16.5 ROI 排序

ROI 优化项 收益 成本 风险 对应章节
极高 点击立即视觉反馈 响应 380ms → 45ms 1 天 低 §13.1
极高 transform 替代 left/top FPS 32 → 60 1-2 周 中(兼容) §14.1
极高 业务逻辑完全异步 主线程不被阻塞 1-2 周 中 §15.1
极高 高频输入防抖 重输入 FPS 翻倍 几天 低 §16.1
高 rAF 驱动动画 丢帧率 18% → 2% 几天 低 §14.3
高 乐观更新 感知响应 0ms 1-2 周 中(回滚) §13.2
高 长任务分片 动画不被打断 1-2 周 中 §15.3
中 will-change 按需启用 动画启动顺滑 几天 中(OOM) §14.2
中 骨架屏 / 占位 感知延迟 -60% 1 周 低 §13.3
中 连续操作合并 服务端 QPS -90% 1 周 中(语义) §16.2
中 滚动监听内禁重活 滚动 FPS 大幅升 1 周 低 §16.3
中 动画期间暂停非关键 CPU 留给关键 1 周 低 §16.4
中 120Hz 适配 高刷设备体验 +30% 2-3 周 中(配套) §14.4
中 避免动画期间 setState 动画 P99 -50% 1-2 周 中(时序) §15.2

# 16.6 避免反向收益

  • setTimeout 模拟 60fps:§17.2丢帧 18%(§02 案例第 1 周翻车)。
  • 缩短动画治卡顿:用户感觉突兀(第 2 周翻车)。
  • 全开 will-change:层爆炸 OOM(第 3 周翻车)。
  • 滚动监听里强制 layout:FPS 杀手。
  • 120Hz 不配套:比 60Hz 还差。

探索性思考:为什么"防抖 + 批量"是高频输入的"必修课"?因为它们解决了"事件频率 > 处理能力"的物理矛盾。用户键入 100 字符 = 100 次事件 = 100 次浪费的搜索请求 —— 不防抖等于让 CPU 做 99 次无用功。

▶▶ 回扣 §02 案例:方法派 Day 4 第 4 层"防抖批量"让连续加购合并触发——避免每次加购都全链路。性能优化的精髓是"做更少的事,而不是更快地做事"。


# 17.求证实验 ⭐

本节是"为什么这些优化生效"的实证基础。每个实验严格遵循"6 步求证法"。

# 17.1 实验一:transform vs left/top

猜想:left/top 和 transform 性能差不多。

假设:transform 走 GPU 合成器,比 left/top 快 5 倍。

执行:

实现 FPS 主线程 CPU
left/top 32 95%
transform 60 12%

验证:

  • transform 走合成器,主线程几乎不参与。
  • left/top 触发 layout + paint,主线程被打满。

思考:

  • 所有位移/缩放/旋转动画一律用 transform,opacity 同理。
  • will-change: transform 提前晋升合成层,但开太多会爆显存。

# 17.2 实验二:rAF 时机决定卡顿率

猜想:setTimeout 和 rAF 都能驱动动画,差异不大。

假设:rAF 卡顿率明显低于 setTimeout。

执行:

驱动方式 丢帧率
setTimeout(16) 18%
requestAnimationFrame 2%

验证:

  • setTimeout 与 vsync 不对齐导致漂移。
  • rAF 严格对齐 vsync。

思考:

  • 所有动画必须用 rAF 驱动;定时器只用于业务逻辑。
  • iOS 上 CADisplayLink 是同等机制;Android Choreographer.postFrameCallback。

# 17.3 实验三:100ms 黄金线

猜想:用户对 100-200ms 延迟差异不敏感。

假设:< 100ms 用户无感,> 100ms 急剧下降。

执行:

输入响应延迟 用户主观分(1-5)
50ms 4.7
100ms 4.3
150ms 3.2
200ms 2.1

验证:

  • 100-150ms 是断崖。
  • 这是 Doherty 阈值的真实证据。

思考:

  • 输入响应 SLO 必须 ≤ 100ms(P95),不要设 200ms。
  • Doherty 阈值(< 400ms)是"思维不中断",但只是底线,不是优秀。

▶▶ 回扣 §02 案例:本实验"100-150ms 是用户感知断崖"是案例 380ms 翻车的直接证据——主观分从 4.3 → 3.2 完全符合实验数据。

# 17.4 实验四:防抖与分片

猜想:高频输入场景下防抖收益有限。

假设:防抖 + 长任务分片可让重输入场景 FPS 翻倍。

执行:

场景 无防抖无分片 仅防抖 100ms 防抖 + 分片 5ms
搜索框打字 FPS 18 FPS 42 FPS 58
滑动滚动 FPS 28 FPS 45 FPS 60
点赞连击 FPS 22 FPS 50 FPS 60

验证:

  • 防抖 + 长任务分片让重输入场景 FPS 翻倍。

思考:

  • 所有高频输入必须配合防抖 + 长任务分片。
  • 搜索 300ms 防抖,滚动 16ms throttle,点赞 100ms 合并。

# 17.5 实验五:120Hz 高刷屏

猜想:120Hz 总是更流畅。

假设:120Hz 设备配套优化才有收益;未优化反而比 60Hz 更糟。

执行:

屏幕 FPS 帧预算 同动画感知 应用层 CPU 增量
60Hz 16.7ms 流畅 baseline
120Hz 8.3ms 明显更顺滑(盲测胜率 78%) +35%
120Hz + 未优化 8.3ms(超时) 抖动比 60Hz 还差 +50%

验证:

  • 120Hz 设备配套优化才有收益。
  • 未优化反而比 60Hz 更糟。

思考:

  • 每帧任务必须 ≤ 5ms。
  • 非动画场景可降回 60Hz 省电。

# 17.6 五大实验启示

   transform vs left/top   → 走合成器,FPS 32 → 60        ─┐
   rAF vs setTimeout       → 丢帧率 18% → 2%               │
   100ms 黄金线            → 100-150ms 是用户感知断崖     ├─▶ 动画 = 合成 + rAF + 反馈快 + 防抖 + 高刷适配
   防抖 + 长任务分片       → 重输入 FPS 翻倍              │
   120Hz 真实提升          → 配套优化才有收益              ─┘
1
2
3
4
5

统一启示:

  • 走合成器是物理决定的"丝滑保证":transform/opacity 是必修课。
  • vsync 对齐是基础:rAF / Choreographer / CADisplayLink 必用。
  • 100ms 是物理感知断崖:不是工程参数。
  • 高频输入必须防抖:不防抖 = 让 CPU 做无用功。
  • 高刷需配套:未优化反而更糟。

▶▶ 回扣 §02 案例:方法派 5 天闭环每一步都对应本节实验。实验是优化前的"必经之路"。


# 18.实战案例

# 18.1 跨端同构案例:列表滚动卡顿

背景:某 App 列表页滑动卡顿,FPS 降到 25。

根因:

  • Android:onScroll 回调里做了 measureText 计算(强制同步布局)
  • iOS:scrollViewDidScroll 内调用 layoutIfNeeded
  • Web:scroll handler 内 getBoundingClientRect

治理:

  • 统一原则:滚动监听里只做读、不做写、不做计算
  • Android:把测量移到滚动结束后;中间状态用预估值
  • iOS:用 contentOffset 替代 layout 计算
  • Web:IntersectionObserver 替代 scroll handler

效果:

平台 滚动 FPS(before) 滚动 FPS(after)
Android 25 58
iOS 35 60
Web 22 55

核心洞察:滚动监听里只做读、不做写、不做计算——这是跨端通用法则。

# 18.2 平台特异案例:iOS 转场动画掉帧

背景:某 iOS App 自定义转场动画在低端机掉帧严重。

根因:Instruments 看到 Core Animation 提交栈每次都触发 layoutIfNeeded。

治理:

// 反例:动画期间隐式触发布局
UIView.animate(withDuration: 0.3) {
    self.view.transform = ...
    // 自动布局可能在这里触发 layoutIfNeeded
}

// 正例:动画前显式确保布局完成
self.view.layoutIfNeeded()  // 强制完成布局
UIView.animate(withDuration: 0.3) {
    self.view.transform = ...
}
1
2
3
4
5
6
7
8
9
10
11

效果:转场动画 FPS 30 → 60。

洞察:UIKit 的隐式布局会污染动画路径,动画前必须确保布局已稳定。

# 18.3 反例案例:will-change 滥用

背景:某 App 团队听说 will-change 能加速动画,给所有动画元素加上。

结果:

  • 中端机内存涨 60MB
  • 低端机直接 OOM 崩溃
  • 动画也没明显改善(因为没有 transform 动画)

修复:

/* 反例:全开 */
* { will-change: transform; }

/* 正例:仅对真正会动画的元素,且动画结束后释放 */
.box { will-change: transform; }

/* JS 控制:动画前加,动画后移除 */
element.style.willChange = 'transform';
animate(element).then(() => {
    element.style.willChange = 'auto';
});
1
2
3
4
5
6
7
8
9
10
11

洞察:will-change 是"提前提层"的提示——只对真正会动画的元素有意义。性能优化的基本原则:精准而非全量。


# 19.防劣化体系

# 19.1 三道防线总览

   ┌────────────┐     ┌────────────┐     ┌────────────┐
   │ 编码期 Lint │  →  │ CI 卡口     │  →  │ 线上 SLO    │
   │ IDE 即时提示│     │ Macrobench  │     │ 监控告警    │
   └────────────┘     └────────────┘     └────────────┘
1
2
3
4

# 19.2 编码期 Lint

自定义规则:

  • 动画用 left/top → 警告(建议 transform)
  • setTimeout 间隔 16ms → 警告(建议 rAF)
  • scroll handler 内含 getBoundingClientRect → 警告
  • will-change 全局 / * 选择器 → 错误
  • 主线程 IO(StrictMode + Lint)→ 错误
  • 动画期间 setState 改 layout 属性 → 警告

# 19.3 CI 卡口

性能基线测试:

@Test
fun animationBenchmark() = benchmarkRule.measureRepeated(
    metrics = listOf(FrameTimingMetric()),
    iterations = 5
) {
    val button = device.findObject(By.res("addToCart"))
    button.click()
    Thread.sleep(500)  // 动画时长
}
1
2
3
4
5
6
7
8
9

卡口规则:

  • 关键动画 FPS P99 < 55 → 阻断 PR
  • 输入响应 P95 退化 ≥ 20% → 阻断
  • 强制同步布局每帧 > 0 → 警告

# 19.4 线上 SLO

指标 目标 告警阈值
输入响应 P95 ≤ 100ms > 200ms
动画 FPS P95(60Hz) ≥ 55 < 50
动画 FPS P95(120Hz) ≥ 110 < 100
页面切换动画偏差 ≤ 10% > 30%
强制同步布局/帧 0 > 0
用户主观流畅度 > 4.0 < 3.5

# 19.5 文化建设

  • 动画预算:新动画必须申报"FPS 预算"和"输入延迟预算"
  • Code Review:动画相关 PR 必有 perf reviewer
  • OKR:用户主观流畅度进 OKR

探索性思考:为什么"用户主观流畅度"应该是核心 OKR?因为所有客观指标都是代理——FPS 高、延迟低 ≠ 用户满意。主观打分是"地面真值" —— 工程师容易迷恋指标,但产品最终是给人用的。


# 20.跨平台速查

# 20.1 工具速查

平台 帧度量 输入度量 视频对比
Android Choreographer / Macrobenchmark InputDispatcher trace 高速摄像
iOS CADisplayLink / Instruments RunLoop trace 高速摄像
Web rAF + Performance API INP / Performance Observer DevTools recording

# 20.2 关键 API 速查

目的 Android iOS Web
动画驱动 Choreographer / ViewPropertyAnimator CADisplayLink / UIView.animate rAF / CSS Transition
合成层提示 setLayerType shouldRasterize will-change
动态读刷新率 display.refreshRate UIScreen.maximumFramesPerSecond window.screen.refreshRate
防抖 自实现 / Flow.debounce 自实现 lodash.debounce
滚动观察 RecyclerView.OnScrollListener scrollViewDidScroll IntersectionObserver
输入延迟 自打点 自打点 INP
长任务分片 IdleHandler DispatchSourceTimer scheduler.postTask / requestIdleCallback

# 20.3 各平台动画优化清单

Android:

  • [ ] 所有位移动画用 translationX/Y(或 ViewPropertyAnimator)
  • [ ] 所有透明动画用 alpha
  • [ ] 动画前 setLayerType(HARDWARE),结束后 NONE
  • [ ] Choreographer 驱动自定义动画
  • [ ] 滚动监听内禁同步 layout
  • [ ] 业务逻辑全异步(lifecycleScope)
  • [ ] 防抖 + 长任务分片
  • [ ] 高刷设备 ARR 适配

iOS:

  • [ ] 用 transform 替代 frame
  • [ ] UIView.animate 替代手动动画
  • [ ] 动画前 layoutIfNeeded
  • [ ] 避免 scrollViewDidScroll 内 layoutIfNeeded
  • [ ] DispatchQueue 异步业务
  • [ ] ProMotion 适配

Web:

  • [ ] CSS Transition + transform/opacity
  • [ ] will-change 仅动画前启用,动画后释放
  • [ ] 滚动监听用 IntersectionObserver
  • [ ] 防抖 + scheduler.postTask
  • [ ] INP 监控
  • [ ] passive event listener(滚动)

# 21.总结与延伸

# 21.1 五条核心原则

  1. 100ms 黄金线是物理感知断崖:§17.3是核心。
  2. transform 走合成器:§17.1FPS 翻倍。
  3. rAF 严格 vsync:§17.2setTimeout 是禁忌。
  4. 业务异步是基础:§02 案例280ms 阻塞的根因。
  5. 120Hz 必须配套:§17.5未优化反而更糟。

# 21.2 五个常见误区

误区 真相
"FPS 平均 60 就够" 错。P99 才重要
"setTimeout 做动画也行" 错。丢帧 18%
"will-change 全开就快" 错。层爆炸 OOM
"动画快就是体验好" 错。前 100ms 没反馈一样卡
"120Hz 必然更顺" 错。未优化反而抖

# 21.3 一句话总结

动画与交互是"性能给用户的最直接答卷"——指标都达标了但用户还说卡,那一定是这一章没做好。100ms 黄金线 + 合成器加速 + 业务异步 + 高频防抖 = 动画与交互四件套。关键不是动画快,是首次反馈快——Doherty 100ms 是用户感知断崖,前 100ms 没反馈就是"卡"。

# 21.4 延伸阅读

  • 卷三·01 渲染管线与原理:动画的渲染基础
  • 卷三·02 FPS 与帧率检测:动画的量化
  • 卷三·03 卡顿捕获与归因:交互延迟是卡顿特殊场景
  • 卷三·05 页面 UI 与布局优化:UI 优化是动画基础
  • 卷四·04 列表与滚动性能:高频滚动专项
  • 卷二·06 IO 与存储性能:业务异步基础

# 21.5 给团队的建议

  • 第 1 周:审计所有动画,把 left/top 改成 transform
  • 第 2 周:上线输入响应延迟监控,SLO 设 100ms
  • 第 3 周:滚动 / 列表场景做强制同步布局清零
  • 第 4 周:120Hz 设备适配,关键动画做专项调优

下一篇预告:本文是流水线篇最后一篇。继续阅读卷二·资源专项篇 或 卷四·业务专项篇 深入特定主题。

上次更新: 2026/06/07, 10:26:12
页面UI与布局优化
App冷启动优化

← 页面UI与布局优化 App冷启动优化→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式