面试备战 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 滥用。

参考资料