文章

Flutter 渲染性能优化深度解析

Flutter 渲染性能优化清单:const 构造、RepaintBoundary、避免 build 重、减少 Opacity。 讲清每项原理与时机,掌握后能答出「复杂页面为什么会掉帧、怎么治」。

Flutter 渲染性能优化深度解析

一句话概括

Flutter 渲染性能优化的 90% 靠两件事:const 让 Widget 免重建,RepaintBoundary 让绘制免扩散。剩下 10% 是别在 build 里干重活、别滥用 Opacity——但 90% 的工作两招就能搞定。

核心知识点

1. const Widget:一行代码消灭子树重建

1
2
3
4
5
6
7
8
9
10
11
// ❌ 每次父 Widget build,这个 Padding 都创建新实例
Padding(
  padding: EdgeInsets.all(8),  // 新对象
  child: Icon(Icons.star, color: Colors.amber),  // 新对象
)

// ✅ const 关键字:编译时常量,运行时直接复用
const Padding(
  padding: EdgeInsets.all(8),
  child: Icon(Icons.star, color: Colors.amber),
)

为什么快:Flutter 的 Element 在 diff 新旧 Widget 时,如果新旧是同一个 const 实例(引用相等),直接 return——跳过 build、跳过 layout、跳过 paint。子树里 50 个 const Text,一秒省下 50 次 build 调用。

怎么记住加 const:Android Studio / VS Code 装 flutter_lints,不写 const 就黄线警告。习惯了就成肌肉记忆。

2. RepaintBoundary:画一个 “防火墙”

1
2
3
4
5
6
7
8
// 动画按钮每帧重绘,但不会触发 Sidebar 和 Header 的重绘
Row(children: [
  const Sidebar(),         // 永不重绘
  RepaintBoundary(
    child: AnimatedButton(animController),  // 只重绘自己
  ),
  const Header(),          // 永不重绘
])

本质是在 RenderObject 树中创建一个新的 OffsetLayer。边界内的内容重绘时,边界外的 Layer 直接用缓存的位图——相当于一次 “截屏” 后反复用。

该不该用:列表 Item(Flutter 自动加)、独立动画、频繁更新的小区域——必须加。但别所有 Widget 都包一层,一个 RepaintBoundary = 一张离屏位图 = 额外 GPU 内存。

3. FadeTransition > Opacity:一个 Layer 的差别

1
2
3
4
5
// ❌ Opacity 创建离屏缓冲区,然后合成——贵
Opacity(opacity: animation.value, child: HeavyWidget())

// ✅ FadeTransition 直接在合成层调透明度——便宜
FadeTransition(opacity: animation, child: HeavyWidget())

Opacity 需要把子 Widget 先渲染到离屏 buffers,再合成。FadeTransition 在 compositor 阶段直接乘 alpha 值。视觉效果一模一样,GPU 开销差了一个数量级。

4. build 里别干三件事

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ❌ build 里创建 ScrollController——每次 build 都 new,旧的不释放
@override
Widget build(BuildContext context) {
  final controller = ScrollController(); // 内存泄漏制造机
  return ListView(controller: controller, ...);
}

// ✅ 在 initState 创建,dispose 释放
late final _controller = ScrollController();

// ❌ build 里做网络请求——每次重建都发
@override
Widget build(BuildContext context) {
  final data = await api.fetch(); // 阻塞 build
}

// ❌ build 里用 MediaQuery.of(context).size 替代 LayoutBuilder
final screenWidth = MediaQuery.of(context).size.width; // 任何重建都触发
// ✅ 用 LayoutBuilder——只在父级尺寸变化时重建

5. Flutter DevTools 三板斧定位性能瓶颈

  • Performance Overlay:顶部两条线,绿色正常、红色掉帧。谁红看谁
  • Repaint Rainbow:重绘区域闪彩色边框。闪得越欢越要加 RepaintBoundary
  • Track Builds:秒表图标,显示每个 Widget 的 build 耗时。谁慢优化谁

其实你每天都在用

  • AppBar 里的标题文字 — const Text('首页') 整个 AppBar 子树永不重建
  • TabBar 切换 — 每个 Tab 页加 RepaintBoundary,切换不重绘
  • 下拉刷新动画 — RefreshIndicator 内部已经带了 RepaintBoundary
  • 页面切换动画 — FadeTransition 包裹 Hero 动画,GPU 做淡入淡出不碰 CPU
  • 列表的静态 Item — ListView.builder 默认给每个 Item 包了 RepaintBoundary

常见误解(FAQ)

  • ❌ 误区:「const 只是省一点内存」 const 真正省的是 build——两个 const 实例引用相等,Element diff 直接跳过。在一个 60fps 的页面里,一次 build 跳过 = 省 16ms 预算里的几毫秒。

  • ❌ 误区:「RepaintBoundary 越多越好」 每个边界 = 一张离屏纹理 = 额外 VRAM。100 个 RepaintBoundary 在低端机上可能直接 OOM。只在频繁重绘且范围明确的区域用。

  • ❌ 误区:「用函数返回 Widget 和用 StatelessWidget 拆分一样」 函数返回 Widget 没有 Element 边界,父 Widget 重建时函数体整个重新执行。拆成 StatelessWidget,Flutter 的 diff 引擎可以跳过不变的子 Element。

  • ❌ 误区:「Impeller 解决了所有性能问题」 Impeller 解决了着色器编译卡顿(shader jank),但 RepaintBoundary、const、避免 Opacity 这些基本原则不变。Impeller 是更好的引擎,不是魔法。

一句话总结

性能优化的本质不是 “让 Flutter 更快”,而是 “让 Flutter 少干活”。const 告诉它 “这个不用重建”,RepaintBoundary 告诉它 “那个不用重绘”。把这两句话用对了,剩下的交给引擎。

本文由作者按照 CC BY 4.0 进行授权