了解 Flutter 的帧渲染流程和 Jank 产生原因,是性能优化的理论基础。
一、Frame 渲染流程
Flutter 以每秒 60 帧(或 120 帧)的节奏渲染界面,每帧有约 16ms (60fps)或 8ms(120fps)的预算。
每帧渲染流程(Pipeline)
① Build 阶段(Dart/UI Thread)
Widget.build() 被调用
→ 生成/更新 Element 树
→ 创建/更新 RenderObject
② Layout 阶段(Dart/UI Thread)
RenderObject.performLayout()
→ 从根节点递归计算每个 Widget 的尺寸和位置
→ 父节点向子节点传递约束(Constraints)
③ Paint 阶段(Dart/UI Thread)
RenderObject.paint()
→ 将绘制命令记录到 PictureRecorder
→ 生成 Layer Tree
④ Composite 阶段(GPU Thread / Raster Thread)
Layer Tree → Skia/Impeller
→ 生成 GPU 指令
→ 提交给 GPU 渲染
⑤ Display 阶段
GPU 输出像素 → 显示到屏幕
线程模型:
| 线程 | 职责 |
|---|---|
| UI Thread(Dart) | 执行 Dart 代码,处理 Build/Layout/Paint |
| Raster Thread(GPU) | 栅格化,将 Layer Tree 转为 GPU 指令 |
| IO Thread | 图片解码、文件读写 |
| Platform Thread | 处理平台 Channel 调用 |
二、Jank 与帧丢失原因分析
Jank 是指帧渲染时间超出预算(16ms),导致帧被丢弃,用户感知到卡顿。
2.1 UI Thread 卡顿(Build/Layout/Paint 耗时)
| 原因 | 表现 | 解决方案 |
|---|---|---|
| Widget 频繁无效重建 | build() 调用过多 | const 构造函数,精细化 setState |
| 复杂布局嵌套 | Layout 阶段耗时 | 减少嵌套,使用 CustomMultiChildLayout |
| 主线程同步 I/O | 阻塞 UI Thread | 使用 async/await 异步处理 |
| 复杂计算在主线程 | 16ms 内无法完成 | 使用 Isolate 或 compute |
2.2 Raster Thread 卡顿(GPU 渲染耗时)
| 原因 | 表现 | 解决方案 |
|---|---|---|
| Shader 首次编译(Jank) | 首次动画卡顿 | Impeller / SkSL 预热 |
| 过度绘制(Overdraw) | 大量透明层叠加 | RepaintBoundary,减少透明度 |
| 图片过大未压缩 | 内存占用高,解码慢 | 指定 cacheWidth/cacheHeight |
| 大量图层合成 | Composite 费时 | 控制 Layer 数量 |
2.3 帧丢失可视化
正常:|帧1|帧2|帧3|帧4|帧5|... 每帧 ≤ 16ms
丢帧:|帧1| 帧2 |帧3|... 帧2 耗时 32ms,丢失1帧
三、性能指标
| 指标 | 说明 | 目标 |
|---|---|---|
| FPS | 每秒帧数 | 60 fps(流畅),120 fps(高刷) |
| 帧耗时 | 单帧渲染时长 | < 16ms(60fps) |
| Jank 率 | 丢帧比例 | < 1% |
| 启动时间 | 冷启动到首帧 | < 1s(目标) |
| 内存占用 | RSS(实际物理内存) | 根据设备档位分级 |
小结
| 概念 | 要点 |
|---|---|
| 帧预算 | 60fps = 16ms/帧,超时丢帧引发 Jank |
| Build → Layout → Paint → Composite | 四步渲染流水线 |
| UI Thread | 执行 Dart 代码,最容易成为瓶颈 |
| Raster Thread | GPU 渲染,Shader 编译卡顿常见原因 |
| Jank 原因 | 过度重建、同步耗时操作、过度绘制 |
👉 下一节:7.2 性能调试工具