为什么 Flutter rebuild 很频繁,但通常不会很慢?
核心答案是:
Flutter 不是每次都把 UI 从头销毁重建。Widget 会频繁创建,但 Element 和 RenderObject 会尽量复用。
官方文档里对 Element 的定义很关键:Widget 是不可变的 UI 配置,Element 是某个 Widget 在树中特定位置的实例;当父 Widget rebuild 后,只要新旧 Widget 的 runtimeType 和 key 相同,Flutter 会更新原来的 Element,而不是直接销毁重建。
1. 三棵树分别负责什么?
Flutter UI 不是只有一棵 Widget Tree,而是可以理解成三棵树协作:
dart
Widget Tree
描述 UI 长什么样
Element Tree
管理 Widget 在树中的位置、生命周期、State、依赖关系
RenderObject Tree
负责 layout、paint、hit testing
官方文档也说明,Widget 构建递归最终会落到 RenderObjectWidget,它们会创建底层 render tree 节点;Render tree 存储 UI 的几何信息,这些信息在 layout 阶段计算,并在 paint 和 hit testing 阶段使用。
2. Widget:不可变配置
你平时写的这些都是 Widget:
scss
Text('Hello')
Container(
padding: const EdgeInsets.all(12),
child: Text('Flutter'),
)
ListView.builder(...)
Widget 本质上是配置对象。
它描述:
scss
这里需要一个 Text
文字是什么
样式是什么
子 Widget 是谁
但 Widget 自己不负责:
保存 State
计算尺寸
绘制像素
处理命中测试
所以你可以频繁 new Widget。
这也是为什么 Flutter 鼓励你在状态变化时重新描述 UI:
scss
@override
Widget build(BuildContext context) {
return Text(count.toString());
}
每次 count 变了,重新生成一个新的 Text Widget 很正常。
3. Element:真正挂在树上的"实例"
Element 很像 Flutter 内部的"节点实例"。
比如这段:
vbnet
const Text('Hello')
对应的 Widget 是:
scss
Text Widget
但它真正挂到 UI 树上后,会有一个对应的 Element。
你可以这样理解:
dart
Widget 是配置
Element 是这个配置在屏幕某个位置的实际使用
官方 API 文档里还提到:BuildContext 本质上是 Widget 在树中位置的 handle;每个 Widget 都有自己的 BuildContext,而 BuildContext 和 Element 的关系非常近,Element 本身也实现了 BuildContext。
所以你平时写:
javascript
Widget build(BuildContext context) {
return ...
}
这个 context 不是一个普通上下文对象,它代表:
dart
当前 Widget 对应 Element 在树中的位置
这也是为什么 context 不能乱存、不能跨异步随便用。
4. RenderObject:真正干重活的节点
RenderObject 负责更底层的事情:
layout:计算大小和位置
paint:绘制
hit testing:判断点击命中
compositing:合成相关信息
例如:
css
Padding(
padding: const EdgeInsets.all(16),
child: Text('Hello'),
)
表面看是 Widget 嵌套,底层会有对应的 Element、RenderObject 参与布局和绘制。
但是注意:
不是每个 Widget 都一定有自己的 RenderObject。
比如很多组合型 Widget 只是帮你组织结构,它们可能只对应 Element,不一定直接对应 RenderObject。RenderObject Tree 通常是 Element Tree 的子集。Flutter 官方文档也说明,把 Element 树和 RenderObject 树分开有性能、清晰度和类型安全方面的收益。
5. rebuild 到底发生了什么?
假设你有这个页面:
scala
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
const Text('Counter'),
Text('$count'),
ElevatedButton(
onPressed: () {
setState(() {
count++;
});
},
child: const Text('Add'),
),
],
);
}
}
点击按钮后:
ini
setState(() {
count++;
});
大概流程是:
markdown
1. 当前 State 对应的 Element 被标记为 dirty
2. 下一帧 Flutter 调用 build
3. build 返回一批新的 Widget 配置
4. Flutter 用新 Widget 和旧 Element 做匹配
5. runtimeType + key 相同,则复用 Element
6. 对应 RenderObject 只在必要时更新 layout / paint
所以:
rebuild Widget ≠ 销毁整个页面
rebuild Widget ≠ 重新 layout 全部节点
rebuild Widget ≠ 重新 paint 全部节点
6. key 为什么重要?
Element 复用的关键之一是:
vbnet
runtimeType + key
如果没有 key,Flutter 主要按位置和类型匹配。
看这个例子:
less
Column(
children: [
UserCard(name: users[0].name),
UserCard(name: users[1].name),
UserCard(name: users[2].name),
],
)
如果列表顺序变化了,而每个 UserCard 里面又有自己的 State,可能出现 State 跟数据错位。
更好的写法:
less
Column(
children: [
for (final user in users)
UserCard(
key: ValueKey(user.id),
name: user.name,
),
],
)
这样 Flutter 知道:
python
这个 UserCard 对应 user.id
即使位置变了,也尽量复用正确的 Element / State
项目里最常见的场景是:
less
ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return UserCard(
key: ValueKey(user.id),
user: user,
);
},
)
7. 常见误区
误区一:build 执行就说明性能差
不是。
Flutter 的设计就是允许 build 频繁执行。
真正的问题是你在 build 里做了重活。
不要这样:
css
@override
Widget build(BuildContext context) {
final sortedUsers = users..sort((a, b) => a.name.compareTo(b.name));
return ListView(
children: sortedUsers.map((user) => Text(user.name)).toList(),
);
}
问题有两个:
lua
1. sort 是重计算
2. users..sort 会原地修改数据
更好的做法是把计算放到状态层、Repository、Notifier 或缓存结果里。
kotlin
class UserListState {
final List<User> users;
final List<User> sortedUsers;
const UserListState({
required this.users,
required this.sortedUsers,
});
}
Flutter 官方性能建议也明确提到:避免在 build() 里做重复且昂贵的工作,因为祖先 Widget rebuild 时,build() 可能频繁调用。
误区二:把页面写成一个巨大 Widget
不要这样:
scala
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userState = ref.watch(userProvider);
final feedState = ref.watch(feedProvider);
final bannerState = ref.watch(bannerProvider);
final messageState = ref.watch(messageProvider);
return Scaffold(
body: Column(
children: [
// 几百行 UI...
],
),
);
}
}
这样任何一个 provider 变化,都可能让整个大页面 rebuild。
更好的拆法:
scala
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Column(
children: [
HomeHeader(),
HomeBanner(),
Expanded(child: FeedList()),
MessageEntry(),
],
),
);
}
}
class FeedList extends ConsumerWidget {
const FeedList({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final feedState = ref.watch(feedProvider);
return feedState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stackTrace) => Text('$error'),
data: (items) => ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return FeedItem(
key: ValueKey(items[index].id),
item: items[index],
);
},
),
);
}
}
官方性能建议也提到,要避免过大的单个 Widget,并按照封装边界和变化方式拆分;setState() 会导致该 State 的后代 Widget rebuild,所以应该把变化局部化。
误区三:滥用 GlobalKey
GlobalKey 很强,但不要随便用。
很多时候你只是需要列表稳定身份,用:
scss
ValueKey(id)
就够了。
比如:
less
UserCard(
key: ValueKey(user.id),
user: user,
)
不要上来就:
less
UserCard(
key: GlobalKey(),
user: user,
)
GlobalKey 更多用于需要跨树定位、访问 State、保持特殊子树身份的场景。普通列表 item 标识,优先 ValueKey。
8. 实际代码:用 rebuild 边界优化页面
下面这个例子里,count 每秒变化一次。如果整个页面都 watch,它会让 Header、Banner、Footer 都跟着 rebuild。
不推荐
csharp
class DashboardPage extends ConsumerWidget {
const DashboardPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: const Text('Dashboard')),
body: Column(
children: [
const HeaderView(),
const BannerView(),
Text('Count: $count'),
const FooterView(),
],
),
);
}
}
推荐:把变化下沉
scala
class DashboardPage extends StatelessWidget {
const DashboardPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
appBar: DashboardAppBar(),
body: Column(
children: [
HeaderView(),
BannerView(),
CounterText(),
FooterView(),
],
),
);
}
}
class CounterText extends ConsumerWidget {
const CounterText({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('Count: $count');
}
}
这样变化范围更小:
markdown
counterProvider 变化
↓
CounterText rebuild
↓
HeaderView / BannerView / FooterView 不跟着 rebuild
9. 底层理解:为什么 const 有用?
vbnet
const Text('Counter')
const 的意义不是"这个 Text 永远不参与布局"。
它的核心价值是:
arduino
同样配置的 const Widget 可以复用同一个实例
Flutter 在比较新旧 Widget 时更容易快速判断没有变化
减少不必要对象创建
Flutter 的自适应设计最佳实践里也提到,很多小的 const Widget 相比大型复杂 Widget,更有利于 rebuild 性能;同时小 Widget 也更容易阅读和重构。
所以项目里建议:
scss
const SizedBox(height: 12)
const Divider(height: 1)
const Icon(Icons.add)
const Text('确认')
能 const 就 const,但别为了 const 把结构写得很别扭。
10. 项目落地建议
我建议你在项目里建立这几个习惯:
markdown
1. 页面级 Widget 尽量不直接 watch 很多状态
2. 高频变化状态下沉到小 ConsumerWidget
3. 列表 item 有稳定业务 id 时加 ValueKey
4. build 里不排序、不解析大 JSON、不做复杂过滤
5. 能 const 的基础 Widget 尽量 const
6. setState / ref.watch / Provider 监听范围尽量局部化
7. 性能问题不要靠感觉,用 DevTools / Performance Overlay 看
Flutter 官方性能文档也建议使用 Performance Overlay、DevTools Performance View、Dart tracing、benchmark、Widget rebuild profiler 等工具分析 UI 流畅度问题,并指出如果目标是 60fps,一帧大约只有 16ms 预算;支持 120fps 的设备预算更紧。
11. 面试理解点
如果面试官问:
Flutter 里 Widget、Element、RenderObject 有什么区别?
可以这样答:
dart
Widget 是不可变配置,描述 UI 长什么样;Element 是 Widget 在树上某个位置的实例,负责生命周期、State 绑定和依赖关系;RenderObject 负责真正的布局、绘制和命中测试。Flutter rebuild 时通常是重新生成 Widget 配置,然后用 runtimeType 和 key 去匹配旧 Element,能复用就复用,所以 rebuild 不等于整棵树销毁重建。
如果继续问:
为什么 Flutter build 可以频繁执行?
可以这样答:
dart
因为 Widget 很轻量,本质上是不可变配置对象。Flutter 真正长期存在的是 Element 和 RenderObject。build 返回新的 Widget 后,框架会进行 diff / update,复用相同位置、相同类型、相同 key 的 Element,并只在必要时触发布局和绘制。因此 build 频繁不是问题,在 build 里做重计算、让过大的 Widget 监听高频状态才是问题。
12. 今天的小练习
把下面这个页面优化一下:
scala
class UserListPage extends ConsumerWidget {
const UserListPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final users = ref.watch(userListProvider);
final unreadCount = ref.watch(unreadCountProvider);
final banner = ref.watch(bannerProvider);
final sortedUsers = [...users]
..sort((a, b) => a.name.compareTo(b.name));
return Scaffold(
appBar: AppBar(
title: Text('Unread: $unreadCount'),
),
body: Column(
children: [
Text(banner.title),
Expanded(
child: ListView.builder(
itemCount: sortedUsers.length,
itemBuilder: (context, index) {
final user = sortedUsers[index];
return UserCell(user: user);
},
),
),
],
),
);
}
}
目标:
markdown
1. AppBar unreadCount 单独拆成 ConsumerWidget
2. Banner 单独拆成 ConsumerWidget
3. UserList 单独拆成 ConsumerWidget
4. sortedUsers 不要在 build 里每次重复排序
5. UserCell 使用 ValueKey(user.id)
参考方向:
scala
class UserListPage extends StatelessWidget {
const UserListPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
appBar: UserListAppBar(),
body: Column(
children: [
UserBanner(),
Expanded(child: UserListView()),
],
),
);
}
}
class UserListView extends ConsumerWidget {
const UserListView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final users = ref.watch(sortedUserListProvider);
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return UserCell(
key: ValueKey(user.id),
user: user,
);
},
);
}
}
今天记住这一句就够了:
Flutter 优化 rebuild 的核心不是"不要 rebuild",而是"让该 rebuild 的地方 rebuild,不该变的 Element / RenderObject 尽量复用"。