作为一个新手,学习的时候,被很多文章告诫,“千万不要在 build 方法里写耗时逻辑,因为它会被疯狂调用。”
这时往往会疑惑,我只是改了一个小小的计数器,为什么感觉整个屏幕都在刷新?为什么我的 print("build...") 在控制台疯狂输出?
如果你也有这样的疑问,那这篇文章可以参考下。
Flutter 新手最常问的三个问题:
- 为什么 build 会被调用这么多次?
- setState 是不是很耗性能?
- 我这个页面会不会被整页重绘?
一、先说结论,安抚心情
先把最重要的话放在前面:
build 被频繁调用是 正常现象
build ≠ 重绘(可能导致重绘,也可能什么都不发生) setState ≠ 重绘整个页面 ≠ 性能差 Flutter 的性能瓶颈 几乎从来不在 build
日志刷屏 ≠ 页面重绘
二、build到底在干啥
1、build本质只干一件事
Widget build(BuildContext context) {
return ...
}
build 的唯一职责:
👉 根据当前状态,描述 UI 应该长什么样
注意关键词:
- 描述
- 配置
- 声明式
那哪些事儿是build不做的呢?
- ❌ 画界面
- ❌ 请求网络
- ❌ 读文件
- ❌ 初始化业务数据
2、build ≠ 渲染
Flutter 内部是这样分层的:
Widget → Element → RenderObject
(配置) (实例) (真正绘制)
- Widget:轻量、不可变、描述 UI
- Element:Widget 的运行时实例
- RenderObject:真正参与布局和绘制
👉 build 只是在“生成 Widget 配置”
三、build 为什么会被频繁调用?
1、最常见的触发条件(是Flutter的设计,并非bug)
build 会在这些情况下被调用:
- ✅ 父组件 rebuild
- 如果父组件调用了 setState , 它的所有子组件都会跟着重建
- 即使子组件的状态没有变化,也会重新执行 build 方法
- ✅自己调用 setState
- 当你调用 setState(() { ... }) 时,Flutter 会标记当前组件为“脏”状态
- 框架会在合适的时机调用该组件的 build 方法重新构建
setState(() {
count++;
});
- ✅ 屏幕尺寸变化
- 屏幕旋转
- 分屏
- 输入法弹出
- ✅ 依赖的 InheritedWidget 变化
- Theme
- MediaQuery
- Provider(后面会学习)
- ✅ 动画和滚动的“副作用”
- 动画每一帧都会触发状态变化
- 滚动时,列表项的 build 方法也可能被频繁调用
2、真实场景示例:
反例:容易导致频繁重建的代码
class BadExample extends StatefulWidget {
@override
_BadExampleState createState() => _BadExampleState();
}
class _BadExampleState extends State<BadExample> {
int count = 0;
@override
Widget build(BuildContext context) {
print('BadExample build called');
// 问题1:在build中创建新对象
final user = User(name: '乐乐', age: 8);
// 问题2:在build中执行耗时计算
final result = _calculateSomething();
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => setState(() => count++),
child: Text('Increment'),
),
// 问题3:父组件重建导致子组件重建
ChildWidget(),
],
);
}
int _calculateSomething() {
// 模拟耗时计算
return 42;
}
}
class ChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('ChildWidget build called');
return Text('I am a child');
}
}
上述代码,每次点击按钮都会触发。即使 ChildWidget 没有任何变化,也会跟着重建!
BadExample build called
ChildWidget build called.
四、setState 到底干了什么?
1、setState 的真实作用(非常重要)
setState(() {
count++;
});
这行代码 只做了两件事:
1️⃣ 修改状态
2️⃣ 标记当前 Element 为 dirty(需要 rebuild)
❗ setState 不会立刻 build
❗ setState 不会同步重绘
Flutter 会在 下一帧 统一处理所有 dirty 的 Element。
2、setState 的“影响范围”
✅只会影响当前 State 对应的子树
✅setState是局部更新,无法影响父组件、兄弟组件,也不能跨组件层级传递状态。
✅ 不会整个 App 重建,放心。
五、build 很多次,为什么页面不卡?
这算是Flutter的核心优势之一
1️⃣ Flutter 的 diff 机制
每次 build:
- 新 Widget 树
- 对比旧 Widget 树
- 只更新不同的 Element / RenderObject
👉 和 React 的 Virtual DOM 思想相似,但却更激进。
2️⃣ Widget 很便宜(非常便宜)
- Widget 只是普通 Dart 对象
- 创建成本很低
- Flutter 鼓励你 多build,少存状态(很重要的一点,请谨记)
3️⃣ Widget 树 vs Element 树
- Widget 树:每秒可以重建几千个,只是内存里的轻量配置对象。
- Element 树:它是“管家”,会对比新旧 Widget。如果 Widget 类型和 Key 没变,它就复用,只更新属性。
- RenderObject 树:这才是真正的“重活”,涉及布局计算和像素绘制。只要 Element 觉得 Widget 没本质变化,RenderObject 就不会动。
六、真正危险从不是build本身,而是build里面做了什么
❌ 错误示例 1:在 build 里请求网络
Widget build(BuildContext context) {
fetchData(); // ❌
return ...
}
👉 结果:
- 每次 rebuild 都请求网络,可能一秒几十次
- 浪费大量网络资源,数据不停刷新
❌ 错误示例 2:在 build 里初始化数据
Widget build(BuildContext context) {
list = loadFromDb(); // ❌
return ...
}
👉 结果:
- 每次 rebuild 都读取数据库,IO读取拥堵,APP卡爆炸
✅正确示例:使用initState
@override
void initState() {
super.initState();
fetchData();
}
记忆点:
build = 描述 UI
initState = 初始化一次
七、如何避免不必要的重建(性能优化)
1️⃣ 拆分Widget(最重要)
❌ 一个巨大的 build:
Widget build(BuildContext context) {
return Column(
children: [
Header(),
Body(count),
Footer(),
],
);
}
✅ 拆成小组件:
class Header extends StatelessWidget { ... }
class Footer extends StatelessWidget { ... }
👉 Flutter 优化的第一法则:拆组件
2️⃣ const Widget 的意义
- 对于不变的 Widget,使用 const 关键字, 编译期间创建。
- Flutter 会复用同一个 Widget 实例,避免build时重建。
class GoodExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const Text('Hello'); // 使用const
}
}
对比示例
//代码核心节选
Column(
children: [
const Text('标题'),
Text('计数:$count'),
],
)
const Text('标题'):仍会走 build 流程,但由于配置恒定, Flutter 可以直接复用 Widget 实例,跳过 diff 成本,几乎没有性能开销。Text('计数:$count'):随状态变化
👉 这是 Flutter 性能优化的第一步
3️⃣ 局部 Stateful,而不是整页 Stateful
❌ 整页 Stateful
class Page extends StatefulWidget { ... }
✅ 只让需要变化的部分 Stateful
class Counter extends StatefulWidget { ... }
4️⃣ 使用 StatefulBuilder 限制重建(rebuild)范围
问题:当父组件重建时,所有子组件都会跟着重建,即使子组件的状态没有变化。
解决方案:
- StatefulBuilder:为局部状态创建独立的重建范围
- Builder:为特定部分创建独立的构建上下文
实际案例对比: 假设我们有一个页面,里面有一个很重的“背景组件”和一个简单的“计数器”。
❌ 糟糕的做法:全页刷新
点击按钮时,整个 ComplexBackground 都会重新 build,即便它根本没变。
class MyPage extends StatefulWidget {
@override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
int _counter = 0;
@override
Widget build(BuildContext context) {
print("整个页面被重建了!"); // 每次点击都会打印
return Scaffold(
body: Column(
children: [
const ComplexBackground(),
Text('计数器: $_counter'),
ElevatedButton(
onPressed: () => setState(() => _counter++),
child: const Text('增加'),
),
],
),
);
}
}
✅ 优化的做法:使用 StatefulBuilder
通过 StatefulBuilder 把计数器逻辑“包”起来。此时,点击按钮只会触发 builder 闭包内的代码。
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
int localCounter = 0; // 定义在 StatefulBuilder 外
return StatefulBuilder(
builder: (context, setState) {
return Column(
children: [
Text('局部计数器: $localCounter'),
ElevatedButton(
onPressed: () {
setState(() => localCounter++);
},
child: const Text('局部增加'),
),
],
);
},
);
}
}
注意:虽然 StatefulBuilder 很好用,但如果你的局部状态逻辑变得很复杂,代码会变成“嵌套地狱”。这时候,将其抽离成一个独立的 StatefulWidget 类才是最高级的做法。
5️⃣合理使用 Key
问题:先搞懂为什么要在列表中用key?
答案:
假设你有一个列表 [A, B, C],你删除了 A。
❌ 如果没有 Key:
- Flutter 发现第一个位置现在的 Widget 是 B。
- 它对比旧的第一个位置(原先是 A),发现类型一样(都是同一个类),且都没有 Key。
- 它误以为:“A 没消失,只是内容变成了 B”。
- 结果:它强行修改 A 的状态来匹配 B。如果你的组件带状态(比如输入框里的文字、滚动位置),你会发现删了 A,结果 B 的文字消失了,或者状态全乱了。
✅ 如果有 Key:
- Flutter 看到新的列表第一个是
Key('B')。 - 它去旧树里找,发现
Key('B')原来在第二个位置。 - 它直接把旧的 Element 移动到新位置。
- 状态被完美保留,且不需要重新创建底层的渲染对象。
- 性能提升:销毁和创建
RenderObject是昂贵的(涉及内存分配、布局计算、绘制)。移动(Move)一个 Element 的开销远小于重建(Rebuild) 。
场景 | 是否需要 Key | 推荐 Key 类型 |
| ------------------------ | ------------ | ------------------- | |
| 动态列表(增删、排序、过滤) | ✅ 必须 | ValueKey (用数据 ID) |
| 跨树移动组件(如动画切换) | ✅ 需要 | GlobalKey |
请看使用范例:
✅ 正确示范:
// 使用数据自带的唯一 ID 作为 Key
return ListView(
children: items.map((i) => MyItem(key: ValueKey(i.id))).toList(),
);
❌ 错误示范:
// 禁止在 build 方法里生成随机 Key!
// 每次 build 都会导致 key 不匹配,从而强制整个列表销毁重建。
return ListView(
children: items.map((i) => MyItem(key: UniqueKey())).toList(),
);
Key 的本质是:
帮助 Flutter 在新旧 Widget 树中正确匹配 Element,而不是“为了性能而加”
6️⃣ 使用 const 集合
对于不变的列表或映射,使用 const 修饰
// 好的做法
final items = const ['A', 'B', 'C'];
// 坏的做法
final items = ['A', 'B', 'C']; // 每次build都会创建新列表
7️⃣ 缓存复杂计算
- 将耗时操作移到 initState 中
class GoodExample2 extends StatefulWidget {
const GoodExample2({super.key});
@override
State<GoodExample2> createState() => _GoodExample2State();
}
class _GoodExample2State extends State<GoodExample2> {
late Future<String> _userDataFuture; //定义变量
@override
void initState() {
super.initState();
// 只在组件初始化时执行一次
_userDataFuture = fetchUserData();
}
Future<String> fetchUserData() async {
// 模拟网络请求
await Future.delayed(const Duration(seconds: 2));
return "乐乐";
}
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: _userDataFuture, // 使用存储的 Future
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
return Text('User: ${snapshot.data}');
},
);
}
}
8️⃣ 使用 RepaintBoundary
- 对于复杂的、不常变化的 UI 部分,使用 RepaintBoundary 隔离渲染,但容易滥用。
- 避免整个屏幕重绘
- RepaintBoundary 会增加 Layer 数量
- 滥用可能导致内存上涨
- 只适用于复杂、静态、不常变化的区域
九、日志很吓人,但性能完全OK的例子
ListView.builder(
itemCount: 1000,
itemBuilder: (context, index) {
print('build item $index');
return ListTile(title: Text('$index'));
},
);
你会看到疯狂的日志输出。
👉 但是:
- ListView 是懒加载
- 屏幕外的 item 根本没 build
- 滚动时只 build 新出现的项
十、记忆点总结
- build 是 描述 UI
- build ≠ 渲染
- setState = 标记 dirty
- Widget 性能开销很便宜
- 真正贵的是 I/O 和计算
- 拆 Widget 是性能优化的王道
- 业务逻辑写在 build 外,UI 描述写在 build 内。
- 不知道怎么优化?先加个
const,再拆个子组件。