885 字
4 分钟
Flutter 动画性能优化

0. 预期#

  • 目标帧预算:60 Hz ≈ 16 ms/帧;120 Hz ≈ 8 ms/帧(UI 线程 + 栅格线程各占一半)。用 DevTools 的 Performance/Timeline 看 UI/Raster 两条帧柱,定位卡顿尖峰。
  • 调试开关:debugRepaintRainbowEnabled 看重绘;PerformanceOverlay 看两线程耗时。只在 debug 有效。

1. 控制重建范围(Widget 层,收益最大)#

要点#

  • setState 局部化;多拆分小组件;尽量用 const
  • AnimatedBuilder/TweenAnimationBuilder 利用 child 参数,让不受动画影响的子树不随帧重建。

示例#

class SpinningIcon extends StatelessWidget with TickerProviderStateMixin {
const SpinningIcon({super.key});
@override
Widget build(BuildContext context) {
final controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
final rotation = CurvedAnimation(parent: controller, curve: Curves.linear);
return AnimatedBuilder(
animation: rotation,
child: const Icon(Icons.autorenew, size: 24), // 静态子树,不随帧重建
builder: (context, child) => Transform.rotate(
angle: rotation.value * 6.2831853,
child: child,
),
);
}
}

2. 减少重绘 & 避免昂贵效果(渲染层)#

2.1 Opacity/剪裁/离屏层#

  • 少用 Opacity 做动画;优先 AnimatedOpacity / FadeTransition / FadeInImageOpacity 常触发离屏层(saveLayer),会贵。
  • saveLayerBackdropFilterShaderMask 等都可能很贵;能不用就不用。
  • 需要分割重绘时用 RepaintBoundary 隔离易变子树,但别滥用(层次过多也有开销)。

示例:淡入图片用 FadeTransition 而非包一层 Opacity

class FadingImage extends StatefulWidget {
const FadingImage({super.key});
@override
State<FadingImage> createState() => _FadingImageState();
}
class _FadingImageState extends State<FadingImage> with SingleTickerProviderStateMixin {
late final AnimationController _c = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 350),
)..forward();
@override
void dispose() { _c.dispose(); super.dispose(); }
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: CurvedAnimation(parent: _c, curve: Curves.easeOut),
child: const RepaintBoundary(child: FlutterLogo(size: 80)),
);
}
}

2.2 布局 vs 变换#

  • 能用 Transform.translate/scaleSlideTransition 的地方,尽量别用会反复触发布局的 AnimatedPositioned/AnimatedContainer(尤其在复杂列表里)。依据:最佳实践强调控制 build() 成本、避免重复布局与内在尺寸遍历。

3. 资源与图片:把活干在对的地方#

  • 网络/大图:解码前降采样——给 Image.*cacheWidth/cacheHeight(按 DPR 计算),可显著降内存/CPU。
  • 滤镜采样:动画缩放/旋转中,必要时调 filterQuality(none/low/medium/high;默认 medium),在质量与开销之间取舍。
  • 预取:提前 precacheImage(例如页面切换/首帧前,为 Hero/大图做预热)。

4. 列表/滚动中的动画(大场景的关键)#

  • ListView.builder/SliverList 的懒加载;必要时调整 cacheExtent 以平衡预渲染与内存。
  • 滚动很快时延后昂贵任务(图片解码/渲染):Scrollable.recommendDeferredLoadingForContext(context);或使用 ScrollAwareImageProvider
  • Tab/Page 场景保活:确需保留状态时用 AutomaticKeepAliveClientMixin,避免来回重建导致动画与数据抖动。

示例:列表中按需推迟加载图片

class PhotoTile extends StatelessWidget {
final String url;
const PhotoTile(this.url, {super.key});
@override
Widget build(BuildContext context) {
final defer = Scrollable.recommendDeferredLoadingForContext(context);
return SizedBox(
height: 160,
child: defer
? const Center(child: CircularProgressIndicator(strokeWidth: 2))
: Image.network(
url,
fit: BoxFit.cover,
// 解码目标尺寸(示意,结合 DPR 计算)
cacheWidth: (MediaQuery.of(context).size.width * MediaQuery.of(context).devicePixelRatio).toInt(),
),
);
}
}

5. 暂停看不见的动画#

  • 离屏/隐藏时停表:用 TickerMode(enabled: false) 或把不在当前 Tab 的动画子树禁用掉(要求控制器基于 TickerProvider 创建)。
TickerMode(
enabled: isCurrentPage, // 非当前页直接停表,省 CPU/GPU
child: yourAnimatedSubtree,
)

6. 非 UI 线程#

  • Impeller:Flutter 3.27 起,iOS 与 Android(API 29+) 默认启用;它在构建期预编译更少的着色器,显著降低首帧/早期的 shader jank。只有在你主动关闭 Impeller 或使用自定义 FragmentProgram 时,才考虑 SkSL 预热这类补救。
  • Isolate/compute:JSON 解析、图片处理、密集计算不要卡在主 isolate;用 compute()/Isolate 后台做,避免动画掉帧。
// 将重 CPU 的解析放到后台 isolate
final parsed = await compute(parseJson, rawJson);

7. Checklist#

  • DevTools 定位:看 UI/Raster 哪条柱子超时;只在 profile/release 下评估。
  • 限制 rebuild:拆组件、用 constAnimatedBuilder.child
  • 限制 repaint:必要时 RepaintBoundary;少 Opacity/BackdropFilter/saveLayer
  • 列表:builder 懒加载、recommendDeferredLoadingForContext、按需 cacheExtent
  • 图片:cacheWidth/Height 降采样、按需 filterQualityprecacheImage 预热。
  • 隐藏就停:TickerMode(false)
  • 引擎/线程:重活放 compute/Isolate
Flutter 动画性能优化
https://blog.lpkt.cn/posts/flutter-anim-opt/
作者
lollipopkit
发布于
2025-08-18
许可协议
CC BY-NC-SA 4.0