面试备战 Flutter 21:性能优化与卡顿定位
面试备战 Flutter 21:性能优化与卡顿定位
Flutter 性能面试不要一上来背“加 const、用 RepaintBoundary”。中高级回答应该先分层定位:
先判断卡顿发生在 Dart/UI 侧,还是 Raster/GPU 侧;再判断是 build、layout、paint、图片、shader、PlatformView、Channel 还是原生主线程阻塞。
一帧大致链路:
Vsync
-> Dart: animation / build / layout / paint
-> Layer Tree
-> Raster: GPU rasterize / compositing
-> Platform compositor
-> Display
1. 帧预算是多少?
流畅度本质上受帧预算约束:
60fps -> 每帧约 16.67ms
120fps -> 每帧约 8.33ms
如果 UI 侧或 Raster 侧任何一边超过预算,都会出现 jank。
注意:Flutter 不是只有 Dart 代码快就行。Dart build 很快,但图片太大、shader 编译、saveLayer、PlatformView 合成慢,也会掉帧。
2. UI 卡和 Raster 卡怎么区分?
| 类型 | 主要表现 | 常见原因 |
|---|---|---|
| UI/Dart 卡 | build/layout/paint 耗时高,手势响应慢 | build 重计算、状态粒度粗、同步 JSON、列表 item 太复杂 |
| Raster/GPU 卡 | Dart 很快,但上屏慢 | 大图纹理、复杂裁剪、saveLayer、阴影、shader、PlatformView |
| 原生侧卡 | Channel 或 PlatformView 相关页面卡 | iOS main thread 阻塞、SDK 回调慢、线程切换不当 |
| 内存卡 | GC 频繁、图片峰值高、OOM | 大图解码、缓存过大、对象泄漏 |
3. 定位工具顺序
3.1 Profile mode
性能判断必须用 profile 或 release,不能用 debug。
debug 模式有断言、服务扩展、JIT、额外检查,性能不代表真实表现。
3.2 Performance Overlay
Performance Overlay 可以快速看 UI/Raster 两条柱状图:
- UI 线程柱子高:看 build/layout/paint。
- Raster 线程柱子高:看图片、图层、GPU、shader、PlatformView。
- 两边都高:可能页面结构重,也可能内存/原生侧拖累。
3.3 DevTools Performance
DevTools Performance 是主力工具。
重点看:
- Frame chart:哪一帧超预算。
- UI events:build/layout/paint 具体耗时。
- Raster events:栅格化、shader、图片上传。
- Timeline:异步任务、Channel、GC。
- Rebuild stats:哪些 Widget 重建频繁。
3.4 Flutter Inspector
Inspector 用来查结构和约束:
- Widget tree 是否过深。
- Rebuild 范围是否过大。
- 约束是否异常。
- 有没有不必要的嵌套。
- Flex overflow 原因。
3.5 Memory view
看内存问题:
- Dart heap 是否持续增长。
- ImageCache 是否过大。
- GC 是否频繁。
- Controller/Subscription 是否泄漏。
- Native/GPU 内存是否异常。
Flutter 大图 OOM 很多时候不在 Dart heap,而在 decoded image、native memory、GPU texture。
3.6 Xcode Instruments
混合工程或 iOS 插件相关问题,要用 Instruments 看原生侧:
- Main Thread Checker。
- Time Profiler。
- Allocations。
- Leaks。
- Core Animation。
如果 PlatformView 卡,单看 Flutter DevTools 往往不够。
4. UI 侧常见问题
4.1 build 里做重活
错误:
Widget build(BuildContext context) {
final result = jsonDecode(largeJson);
return Text('${result.length}');
}
修复:
- 提前解析。
- 缓存结果。
- 用
compute/Isolate.run。 - 放到 Repository / Notifier。
4.2 状态粒度太粗
如果一个页面级状态变化导致整页 rebuild,就会出现大量无关重建。
优化:
- 拆小 Widget。
- 局部 watch。
- Riverpod 使用
select。 - Provider 使用
Selector/context.select。 - 动画用
AnimatedBuilder的 child 参数缓存静态子树。
4.3 列表 item 太重
错误:
ListView(
children: items.map(buildHeavyItem).toList(),
)
修复:
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ItemCell(item: items[index]);
},
)
进一步优化:
- 固定高度用
itemExtent。 - 可预估高度用
prototypeItem。 - 图片按展示尺寸解码。
- item 内避免 IntrinsicHeight。
- 分页加载。
4.4 Intrinsic 和 shrinkWrap 滥用
IntrinsicHeight / IntrinsicWidth 可能触发额外测量。shrinkWrap 会让滚动视图为了计算自身大小而做更多工作。
在短内容里可以接受,在长列表里要谨慎。
5. Paint/Raster 侧常见问题
5.1 saveLayer
某些效果会触发离屏缓冲:
Opacity包裹复杂子树。Clip.antiAliasWithSaveLayer。ShaderMask。- 某些
ColorFilter。 - 大面积阴影和模糊。
离屏缓冲会增加 GPU 内存和合成成本。
优化:
- 避免对大区域做透明度。
- 能用颜色透明值就不要用整层 Opacity。
- 减少大面积 blur/shadow。
- Clip 行为选择更轻量的方式。
5.2 图片过大
图片内存看解码后像素,不看文件大小:
width * height * 4 bytes
一张 6000 x 4000 图片解码后约:
6000 * 4000 * 4 = 96MB
优化:
Image.file(
file,
cacheWidth: 600,
cacheHeight: 600,
)
原则:
按显示尺寸解码,不按原图尺寸解码。
5.3 RepaintBoundary 怎么用?
RepaintBoundary 用来隔离 repaint。
适合:
- 动画区域。
- 频繁变化的小区域。
- 复杂静态背景。
- 列表中相对独立的 item。
不适合:
- 无脑给所有 Widget 加。
- 加在频繁变化且面积巨大的区域。
- 加太多导致 layer 数膨胀。
面试回答:
RepaintBoundary 不是减少 rebuild,而是减少 repaint 扩散。它通过形成独立 layer,让某个区域重绘时不影响兄弟或父区域,但也会增加 layer 管理成本。
6. Channel 和 PlatformView 性能
6.1 Channel 不传大对象
Channel 适合控制消息,不适合传大数据:
- 传 id。
- 传路径。
- 传业务参数。
- 传缓存 key。
避免:
- 大图片 bytes。
- 高频大数组。
- 视频帧。
- 大量日志流。
6.2 PlatformView 谨慎使用
PlatformView 把原生 View 混入 Flutter 合成体系,会带来额外同步和合成成本。
适合:
- WebView。
- Map。
- Camera。
- 必须使用原生 SDK View 的场景。
不适合:
- 长列表大量 PlatformView。
- 复杂 transform/clip/opacity。
- 可以 Flutter 自绘的普通 UI。
7. 一套可执行的排查流程
第一步:复现条件
- 真机。
- profile mode。
- 固定页面和操作路径。
- 记录设备型号、刷新率、数据量。
第二步:看 UI/Raster 哪边超预算
用 Performance Overlay 或 DevTools。
第三步:按方向深入
UI 高:
- build 次数。
- layout 次数。
- 同步任务。
- 状态粒度。
- 长列表。
Raster 高:
- 图片尺寸。
- saveLayer。
- shader。
- clip。
- PlatformView。
Memory 高:
- ImageCache。
- decoded image。
- controller 泄漏。
- native memory。
第四步:改一项,测一项
性能优化不能“全改完再看”。要保留前后数据:
- 平均 frame time。
- p90/p99 frame time。
- jank count。
- memory peak。
- 页面首帧。
- 滚动帧率。
8. 面试怎么回答?
可以这样答:
我会先用 profile mode 复现,然后看 Performance Overlay 或 DevTools Performance,判断是 UI 线程超预算还是 Raster 线程超预算。UI 超预算通常查 build 重计算、状态粒度、layout/intrinsic、同步 CPU 任务;Raster 超预算通常查大图、saveLayer、clip、shader、PlatformView。内存问题再用 DevTools Memory 和原生 Instruments 看 Dart heap、ImageCache、Native/GPU 内存。
优化上不会只背 const。const 只能减少部分不变 Widget 的创建和 diff 成本,真正有效的是缩小 rebuild 范围、避免 build 副作用、列表懒加载、图片按显示尺寸解码、减少离屏渲染、合理使用 RepaintBoundary、避免 Channel 大对象和 PlatformView 滥用。
参考资料
- Flutter Performance profiling: https://docs.flutter.dev/perf/ui-performance
- Flutter Performance best practices: https://docs.flutter.dev/perf/best-practices
- Flutter DevTools Performance view: https://docs.flutter.dev/tools/devtools/performance
- Flutter DevTools Memory view: https://docs.flutter.dev/tools/devtools/memory
- RepaintBoundary API: https://api.flutter.dev/flutter/widgets/RepaintBoundary-class.html
- Image API: https://api.flutter.dev/flutter/widgets/Image-class.html
- Platform views: https://docs.flutter.dev/platform-integration/ios/platform-views