mcpField
F
flutter-performance

flutter-performance

github.com/zakariaf开发63195 次安装5 周前更新
代码仓库

关于 flutter-performance

flutter-performance 是面向 Flutter 开发的性能优化技能,强调在真实低端设备的 profile 模式下测量。它提供减少重建、降低绘制成本和避免 UI 线程阻塞的实践规则与诊断流程。

主要功能

  1. 01缩小状态变化触发的重建范围
  2. 02长列表采用惰性构建
  3. 03将重计算移出 build()
  4. 04按显示尺寸解码图片
  5. 05使用 DevTools 分析 UI 与栅格线程
  6. 06规范资源释放与绘制优化

适用场景

  1. 01排查低端设备上的掉帧
  2. 02减少大型列表的构建开销
  3. 03诊断动画或图像引发的渲染负担
  4. 04在 profile 模式比较优化前后表现

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/zakariaf/flutter-skills --skill flutter-performance

安装后在 Claude Code 中输入/flutter-performance确认已加载。

详细介绍

Flutter 性能

在入门级设备(低端 Android/较旧的 iPhone)上保持稳定的 60 fps(每帧 <=16 ms;120 Hz 时 <=8 ms)。性能需要在 profile 模式下测量,而不是靠断言得出。减少重建、降低绘制开销,并确保 UI 线程和光栅线程都不被占用,才能获得良好的性能。

不容妥协的规则

  1. 所有合法的内容都使用 const。 重建时会跳过 const 子树——框架会根据对象标识直接短路。将 prefer_const_constructors 保持为错误级别;本可以使用 const 却没有使用的非 const 字面量属于 lint 错误,而非建议。
  2. 将重建范围缩小到发生变化的最小 widget。 当状态对象只有一个字段变化时,绝不要对整个状态对象调用 ref.watch(provider)——应使用 provider.select((s) => s.field)。HUD 计数器的刷新绝不能导致耗时的同级 widget 重建。缩小重建范围是降低构建线程开销最有效的手段。
  3. 长列表一律延迟构建。 对内容可变或无上限的列表,使用 ListView.builder/GridView.builder/sliver。ListView(children: items.map(...).toList()) 会预先构建所有屏幕外的行——除非内容很少且固定,否则不要使用。
  4. 不要在 build() 中执行耗时操作。 不要进行 jsonDecode、排序、正则匹配、DateTime 运算、文件/网络操作或大量内存分配——build() 可能每帧都会运行。在 Notifier/ViewModel 中只计算一次,并将结果缓存到不可变状态中。
  5. 将繁重的 CPU 工作移出 UI isolate。 使用 compute() 或启动的 Isolate 解析大型数据、处理图像或进行复杂计算。阻塞 UI 线程必然导致卡顿;光栅线程无法弥补。
  6. 按照显示区域的大小解码图像。 使用 cacheWidth/cacheHeight 或 ResizeImage,设置正确的资源分辨率,并对首屏图片调用 precacheImage。将 4000 px 的源图解码到 100 px 的显示框中会导致内存激增,让低端手机内存溢出。
  7. 能满足需求时,优先使用开销最低的 widget。 优先用 ColoredBox/DecoratedBox 替代 Container,用 SizedBox 实现间距,在高频路径中用 FadeTransition/AnimatedOpacity 替代 Opacity widget。避免在滚动列表中使用 ClipPath/saveLayer——它们会强制光栅线程创建离屏缓冲区。
  8. 在开销较大且独立重绘的子树周围使用 RepaintBoundary(例如动画、图表、实时指示器),这样它的重绘就不会导致相邻内容重新光栅化。不要到处添加边界——每个边界都是会占用内存的合成器层。
  9. 通过 child: 传入耗时的子级。 AnimatedBuilder/ValueListenableBuilder/StreamBuilder 只会重建 builder;将不变的子树通过 child: 传入,使其只构建一次,而不是每帧都构建。
  10. 及时释放资源,避免泄漏。 AnimationController、手势识别器、流订阅、计时器、TextEditingController、缓存等,都应在 dispose()(widget)或 ref.onDispose(provider)中释放。会话作用域的状态应设置为 autoDispose,避免在离开屏幕后继续存活。
  11. 使用 DevTools,在真实的入门级设备上以 PROFILE 模式测量。 Debug 模式下的耗时没有参考意义(JIT、断言)。除了 UI 线程,还要关注光栅线程——saveLayer/模糊/裁剪的开销会体现在线程上,而非 Dart 中。绝不要发布一个仅凭断言得出、未经测量的帧耗时数据。

缩小重建范围

监听发生变化的单个字段,而不是整个状态对象:

// Rebuilds only when the count changes, not on every OrderState mutation.
class OrderBadge extends ConsumerWidget {
  const OrderBadge({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(orderProvider.select((s) => s.items.length));
    return Text('$count');
  }
}

.select 会在每次发布时运行选择器,但只有选定值的 == 发生变化时才会触发重建,因此其输入必须是具有正确相等性判断的不可变值。关于 watch/read/listen 的区别,以及 family + autoDispose,请参阅 state-management-riverpod。

将耗时的子级移出动画循环

// The card is built once; only the transform recomputes each tick.
AnimatedBuilder(
  animation: _controller,
  child: const ProductCard(),
  builder: (_, child) =>
      Transform.scale(scale: _controller.value, child: child),
);

延迟构建列表并调整图像尺寸

// Lazy — only visible rows are built.
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, i) => ItemTile(item: items[i]),
);

// Decode to the slot, not the source resolution.
Image.asset('assets/hero.png', cacheWidth: 240);

将繁重工作移出 UI isolate

// parseItems is a top-level or static function (isolate entry-point rules).
final items = await compute(parseItems, jsonString);

自定义绘制

对于 CustomPainter,应确保 paint() 零分配,shouldRepaint 只对不可变场景值进行一次比较,并使用 repaint: Listenable(即 AnimationController),而不是在 ticker 中调用 setState。相关做法详见 custom-canvas-and-gestures;这里需要牢记的一条性能关键规则:绝不要在 paint() 中分配 Paint/Path/Gradient——将它们保留为 painter 字段,并只修改开销低的属性。

性能分析工作流

  1. 在实体设备上运行 flutter run --profile。
  2. 打开 DevTools -> Performance;重现卡顿时录制。
  3. 对每个超出预算的帧,检查耗时发生在 Build、Layout 还是 Raster。
    • Build 开销大 -> 使用 .select 缩小重建范围,添加 const,将工作移出 build()。
    • Raster 开销大 -> 减少 Opacity/裁剪/saveLayer,添加 RepaintBoundary,简化着色器。
  4. 使用“Track widget rebuilds”查找重建过于频繁的 Widget。
  5. 修复一个瓶颈后重新测量。绝不要在没有前后测量数据的情况下优化。

反模式

  • 在 debug 模式下判断性能——JIT 和断言会让计时结果失真。始终使用 profile 模式和真实设备。
  • 没有性能分析数据就过早优化——只会增加复杂性,无法解决问题;先测量。
  • 在树的高层调用 setState(),因微小变化重建整个屏幕——缩小状态范围。
  • 叶节点中使用 ref.watch(provider) 监听整个对象,而实际只修改了一个字段——使用 .select。
  • 长列表或无界内容使用 ListView(children: [...].toList())——会构建所有屏幕外的行。
  • 在 build() 中进行 jsonDecode/排序/DateTime 运算/正则处理——这些操作可能每帧都运行;在 ViewModel 中预先计算。
  • 在滚动列表中使用 Opacity Widget 实现淡入淡出——会强制执行 saveLayer;改用 FadeTransition 或通过颜色实现淡化。
  • 将完整分辨率图像解码后用于小型 Widget——会导致内存激增,并使低端手机 OOM。
  • 在 UI isolate 上解析大型负载或执行繁重循环——会冻结帧;使用 compute。
  • 到处添加 RepaintBoundary——会造成图层内存膨胀;仅在会独立重绘的子树周围有针对性地使用。
  • 使用 AnimatedBuilder/StreamBuilder 重建开销大的子项,而不是通过 child: 传入该子项。
  • 每次按键都触发重建却不做 debounce,或使用无界缓存/监听器导致泄漏。
  • 在 paint() 中分配 Paint/Path——会产生逐帧垃圾;将它们保留为字段。

完成标准

  • 在所有允许的位置应用 const;prefer_const_constructors 检查通过。
  • 使用 .select 限定重建范围;一个 Widget 中的 tick 绝不会导致开销大的同级 Widget 重建。
  • 长列表或可变长度列表采用惰性构建(.builder/slivers)。
  • build() 中没有 I/O、解析或繁重计算;大型任务通过 compute/Isolate 执行。
  • 图像尺寸与显示尺寸匹配(cacheWidth/ResizeImage);预缓存首屏内容。
  • 选用开销最低且合适的 Widget/效果;有针对性地使用 RepaintBoundary;通过 child: 传入开销大的子项。
  • 任何 CustomPainter 在 paint() 中都不分配对象;shouldRepaint 只进行一次值比较。
  • 已释放控制器、识别器、订阅和计时器;已对作用域内的会话状态调用 autoDispose;无泄漏。
  • 通过 DevTools 在 profile 模式下的实体设备上完成测量——UI 和 raster 线程都在预算内;没有超过 16 ms 的帧;性能结论有录制数据支撑。

相关技能

  • state-management-riverpod — watch/read/listen 的划分、.select、family + autoDispose,以及确保 .select 正确工作的不可变状态。
  • widget-composition — 使用小型 const Widget 类而非 _buildX 方法、遵循 dispose 规则,以及选择开销最低的 Widget。
  • custom-canvas-and-gestures — 零分配的 paint()、只进行一次值比较的 shouldRepaint、repaint: Listenable。
  • testing-strategy — 通过注入时钟实现纯核心逻辑,从而能够在 UI isolate 之外测试繁重计算。

参考资料

评论

支持 Markdown