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
/FadeInImage
。Opacity
常触发离屏层(saveLayer
),会贵。 saveLayer
、BackdropFilter
、ShaderMask
等都可能很贵;能不用就不用。- 需要分割重绘时用
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/scale
、SlideTransition
的地方,尽量别用会反复触发布局的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 的解析放到后台 isolatefinal parsed = await compute(parseJson, rawJson);
7. Checklist
- DevTools 定位:看 UI/Raster 哪条柱子超时;只在 profile/release 下评估。
- 限制 rebuild:拆组件、用
const
、AnimatedBuilder.child
。 - 限制 repaint:必要时
RepaintBoundary
;少Opacity
/BackdropFilter
/saveLayer
。 - 列表:
builder
懒加载、recommendDeferredLoadingForContext
、按需cacheExtent
。 - 图片:
cacheWidth/Height
降采样、按需filterQuality
、precacheImage
预热。 - 隐藏就停:
TickerMode(false)
。 - 引擎/线程:重活放
compute
/Isolate
。
Flutter 动画性能优化
https://blog.lpkt.cn/posts/flutter-anim-opt/