Flutter 流畅度优化组件“Keframe”
通过帧分裂渲染,优化页面切换或复杂列表快速滚动等场景下的卡顿问题。
以下是在 VIVO X23 (骁龙 660) 上运行的示例(可以直接下载)。相同操作下,200 帧优化前后的收集数据指标对比(演示在文章末尾)
| 优化前 | 优化后 |
|---|---|
监控工具来自:fps_monitor
- 流畅:FPS 大于 55,表示每帧时间小于 18ms
- 良好:FPS 在 30-55 之间,即每帧时间 18ms-33ms
- 轻微卡顿:FPS 在 15-30 之间,即每帧时间 33ms-67ms
- 卡顿:FPS 小于 15,表示每帧时间大于 66.7ms
使用帧分裂优化后,卡顿帧数从平均 33.3 帧降低到 200 帧中仅出现 1 帧,轻微卡顿从 188ms 降低到 90ms。卡顿现象大幅减少,流畅帧比例显著提高,整体性能更平滑。以下是详细信息。
| 优化前 | 优化后 | |
|---|---|---|
| 平均出现卡顿的帧数 | 33.3 | 200 |
| 平均轻微卡顿的帧数 | 8.6 | 66.7 |
| 耗时最长 | 188.0ms | 90.0ms |
| 平均耗时 | 27.0ms | 19.4ms |
| 流畅帧比例 | 40% | 64.5% |
页面切换流畅度提升
打开页面或 Tab 切换时,系统会渲染整个页面并通过动画完成页面切换。对于复杂的页面,也会出现帧卡顿。
通过框架组件,页面构建被分解成一帧一帧的,并通过 DevTools 中的性能工具进行查看。切换期间,单帧的峰值从 **112.5ms 降低到 30.2ms**,整个切换过程更加流畅。
如何使用
项目依赖:
在 pubspec.yaml 中添加 keframe 的依赖
dependencies:
keframe: version
组件区分普通版本和 null-safe 版本
普通版本使用:1.0.2
Null-safe 版本使用:2.0.2
快速学习
如下图所示:
假设页面现在由 A、B、C、D 四部分组成,每部分耗时 10ms,页面构建共耗时 40ms。
使用 frameseparateWidget 组件嵌套每个部分。页面第一帧渲染一个简单的占位符,后续四帧分别渲染 A、B、C、D。
对于列表,将 FrameSeparateWidget 嵌套在每个 Item 中,并将 ListView 嵌套在 SizeCacheWidget 中。
构造函数说明
FrameSeparateWidget:一个帧分裂组件,在单帧内渲染嵌套的 widget。
| 类型 | 名称 | 必需 | describe |
|---|---|---|---|
| 按键 | key | no | |
| 整数 | index | no | 帧组件 ID,在使用 SizeCacheWidget 的场景下会传递,并在 SizeCacheWidget 中保持对应的 Size 信息索引 |
| Widget | child | 是 | 实际需要渲染的 widget |
| Widget | placeHolder | no | 占位符 widget,尽量设置一个简单的占位符,默认为 Container() |
SizeCacheWidget:缓存 **帧组件在子节点中嵌套的实际 widget 的大小** 信息。
| 类型 | 名称 | 必需 | describe |
|---|---|---|---|
| 按键 | key | no | |
| Widget | child | 是 | 如果在子节点中包含帧组件,那么 **缓存的是实际 widget 的大小** |
| 整数 | estimateCount | no | 预估屏幕上的子节点数量,可以提高快速滚动的响应速度 |
示例说明
卡顿页面通常是多个复杂 widget 同时渲染。通过对复杂 widget 嵌套 FrameSeparateWidget。渲染时,框架组件在第一帧同时渲染多个 palceHolder,然后在后续帧中渲染复杂的子项,以提高页面流畅度。
例如
ListView.builder(
itemCount: childCount,
itemBuilder: (c, i) => CellWidget(
color: i % 2 == 0 ? Colors.red : Colors.blue,
index: i,
),
)
cellWidget 的高度为 60,内部嵌套了三个 TextField 组件(整体构建时间约 9ms)。
优化过程只需为每个 Item 嵌套框架组件,并为每个 Item 设置 placeHolder(占位符应尽可能简单,并且看起来像实际的 Item)。
在 ListView 的场景下,建议嵌套 SizeCacheWidget,并预加载 cacheExtent 设置得更大一些,例如 500(默认为 250),以提高滑动缓慢的体验。
例如
SizeCacheWidget(
child: ListView.builder(
cacheExtent: 500,
itemCount: childCount,
itemBuilder: (c, i) => FrameSeparateWidget(
index: i,
placeHolder: Container(
color: i % 2 == 0 ? Colors.red : Colors.blue,
height: 60,
),
child: CellWidget(
color: i % 2 == 0 ? Colors.red : Colors.blue,
index: i,
),
),
),
),
以下是几种场景
1. 列表中实际 Item 的大小已知
如果实际 Item 的高度已知(每个 Item 高度为 60),只需设置占位符以匹配实际 Item 的高度即可。参见示例中的帧优化 1。
FrameSeparateWidget(
index: i,
placeHolder: Container(
color: i % 2 == 0 ? Colors.red : Colors.blue,
height: 60,// Keep the same height as the actual item
),
child: CellWidget(
color: i % 2 == 0 ? Colors.red : Colors.blue,
index: i,
),
)
2. 列表中实际 Item 的大小未知
在实际开发中,列表通常是根据数据动态渲染的,一开始无法预知 Item 的大小。
例如,在示例帧优化 2 中,placeHolder(高度 40)与实际 Item(高度 60)大小不一致。
由于每个 Item 在不同帧中渲染,列表会发生 抖动。
可以为占位符设置一个大概的高度。并将 ListView 嵌套在 SizeCacheWidget 中。
对于已经渲染过的 widget,强制设置 palceHolder 的大小,并增大 cacheExtent。这样,渲染过的 Item 在前后滑动时就不会跳跃。
例如,示例中的帧优化 3。
SizeCacheWidget(
child: ListView.builder(
cacheExtent: 500,
itemCount: childCount,
itemBuilder: (c, i) => FrameSeparateWidget(
index: i,
placeHolder: Container(
color: i % 2 == 0 ? Colors.red : Colors.blue,
height: 40,
),
child: CellWidget(
color: i % 2 == 0 ? Colors.red : Colors.blue,
index: i,
),
),
),
),
实际效果如下:
3. 预估屏幕上的 Item 数量
如果能大致预估屏幕上最多能显示的实际 Item 数量,例如 10 个。则将 SizeCacheWidge 的 estimateCount 属性设置为 10*2。快速滚动场景下,构建响应更及时,内存也更稳定。
例如,示例中的帧优化 4。
...
SizeCacheWidget(
estimateCount: 20,
child: ListView.builder(
...
此外,还可以为 Item 嵌套透明度/位移等动画,以优化视觉效果。
实际效果如下:
![]() |
|---|
4. 非列表场景
非列表场景通常不会有流畅度问题,但首次进入时仍可能出现卡顿。
同样,可以将复杂模块分解成不同帧进行渲染,以避免首次进入的延迟。
例如,我们在优化示例中将底部操作区域嵌套了框架组件。
FrameSeparateWidget(
child: operateBar(),
index: -1,
)
帧分裂的成本
当然,帧分裂方案并非完美。在我看来,主要有两个成本
-
额外的构建成本:整个构建过程的构建成本从“N * Widget 成本”变为“N * (Widget + Placeholder 成本) + 系统调度 N 帧的成本”。可以看出,额外开销主要来自 Placeholder 的复杂性。如果 Placeholder 是简单的 Container,经过测试,整体构建时间可能会增加 10-20%。对于当今的移动设备来说,这种额外的开销可以忽略不计。
-
视觉变化:如上文所示,组件会分帧渲染 Item,页面在视觉上会占据空间,直到变为实际的 widget。
但实际上,由于列表存在于缓存区域(建议增大 cacheExtent),用户在高配置设备或正常滑动的情况下不会感觉到。
在低端设备上,快速滑动可能会感觉到过渡,但比严重的卡顿要好。
如有任何疑问,请随时与我联系。如果这启发了您,请不要忘记点赞 ✨✨✨✨ 谢谢!
优化前后演示
| 优化前 | 优化后 |
|---|---|
![]() |

