三棵树:Widget、Element 与 RenderObject
了解 Flutter 如何在重建过程中协调组件树、元素树和渲染树。
三棵树:Widget、Element 与 RenderObject 是 CoddyKit 上的免费 Flutter Mobile Development 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Flutter Mobile Development 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Flutter Mobile Development 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Why Three Trees?
When you write Flutter UI, you describe widgets. But the framework actually maintains three parallel trees that work together every frame:
- Widget tree — immutable configuration objects you build in
build(). - Element tree — the mutable, long-lived bridge that holds state and tracks position in the tree.
- RenderObject tree — the objects that actually do layout, painting, and hit-testing.
Understanding this split is the key to reasoning about rebuild cost, why const widgets are cheap, and why a misplaced Key can corrupt state.
Widgets Are Immutable Blueprints
A Widget is just a lightweight, immutable description of a piece of UI. It holds no mutable state and is cheap to create and discard. Calling build() throws away the old widget objects and produces fresh ones every time.
Because widgets are throwaway, comparing two widgets is fast. Flutter uses that comparison to decide whether the heavier element and render objects can be reused rather than rebuilt.
class Greeting extends StatelessWidget {
const Greeting({super.key, required this.name});
final String name;
@override
Widget build(BuildContext context) {
// A brand-new Text widget is created on every rebuild.
return Text('Hello, $name');
}
}Elements Are the Living Tree
An Element is created by a widget via createElement(). Unlike widgets, elements are mutable and long-lived. The element tree is the actual runtime structure Flutter walks every frame.
Each element holds a reference to its current widget. When a rebuild happens, the element is handed a new widget and decides whether to keep itself (update in place) or be replaced. This is where reconciliation happens.
StatelessElement— backs aStatelessWidget.StatefulElement— owns theStateobject, which is why state survives rebuilds.
State Lives in the Element, Not the Widget
This is the crux of why Flutter's design works. The State object is held by the StatefulElement, which persists across rebuilds. The StatefulWidget itself is replaced on every parent rebuild — but the element (and its state) stays.
That is why your counter does not reset when the parent rebuilds: the widget is new, but the element and its State are the same instance.
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0; // Survives parent rebuilds because the element persists.
void _increment() => setState(() => _count++);
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: _increment,
child: Text('Count: $_count'),
);
}
}RenderObjects Do the Heavy Lifting
Not every widget creates a render object. Only RenderObjectWidgets (like Padding, Opacity, Text's internals) produce entries in the render tree. Composition widgets like StatelessWidget and StatefulWidget only orchestrate other widgets — they have no render object.
RenderObjects implement performLayout(), paint(), and hit-testing. They are the most expensive objects, so Flutter works hard to update them in place instead of recreating them.
- Widget tree: deep (lots of composition widgets).
- Element tree: same depth (one element per widget).
- Render tree: shallower (only render-object widgets appear).
Reconciliation: updateChild
The reconciliation algorithm lives in Element.updateChild(oldElement, newWidget). For each child position, Flutter decides among four outcomes based on the new widget and the old element:
- Update in place — if the widgets can match, reuse the element and render object.
- Replace — if they can't match, deactivate the old subtree and inflate a new one.
- Insert — new widget, no old element.
- Remove — old element, no new widget.
The cheap path (update in place) is what keeps Flutter fast.
The Matching Rule: canUpdate
Whether an element can be reused is decided by the static method Widget.canUpdate(oldWidget, newWidget). The rule is simple but critical:
Two widgets match if and only if their runtimeType and key are equal.
If they match, the existing element keeps its position and render object and just swaps in the new widget's configuration. If they don't match, the old element is discarded and a fresh subtree is inflated — losing any associated State.
// This is the actual decision rule used during reconciliation.
bool canUpdate(Object oldType, Object? oldKey, Object newType, Object? newKey) {
return oldType == newType && oldKey == newKey;
}
void main() {
// Same type, same (null) key -> reuse element.
print(canUpdate('Text', null, 'Text', null)); // true
// Different type -> rebuild subtree, state is lost.
print(canUpdate('Text', null, 'Container', null)); // false
// Same type, different keys -> NOT a match.
print(canUpdate('Text', 'a', 'Text', 'b')); // false
}Why const Widgets Skip Rebuilds
When you mark a widget const, the same canonical instance is reused across builds. During reconciliation, updateChild sees that identical(oldWidget, newWidget) is true and can short-circuit the entire subtree — no element update, no render-object work.
This is why adding const to leaf widgets is one of the cheapest performance wins: identical widget references let Flutter prune whole branches from reconciliation.
class Header extends StatelessWidget {
const Header({super.key});
@override
Widget build(BuildContext context) {
// The const child is identical across rebuilds, so its element
// subtree is skipped entirely during reconciliation.
return const Padding(
padding: EdgeInsets.all(16),
child: Text('Settings'),
);
}
}Keys Disambiguate Same-Type Siblings
When a list contains multiple siblings of the same type, Flutter matches them by position by default. If you reorder them, the elements (and their state) stay glued to the old position — a classic source of bugs in stateful lists.
A Key changes the matching identity. With keys, reconciliation matches by (runtimeType, key) instead of position, so elements and their State follow the widget to its new slot. Use ValueKey for stable data identity.
class ReorderableTiles extends StatelessWidget {
const ReorderableTiles({super.key, required this.items});
final List<String> items;
@override
Widget build(BuildContext context) {
return Column(
children: [
for (final id in items)
// ValueKey lets each element's State follow its data on reorder.
TodoTile(key: ValueKey(id), id: id),
],
);
}
}GlobalKey: Moving an Element Across the Tree
A LocalKey (like ValueKey) only disambiguates siblings under the same parent. A GlobalKey is unique across the entire app and lets an element — with its render object and state intact — be moved to a completely different parent without being rebuilt.
GlobalKeys are powerful but costly: they force the framework to track the element globally and trigger a deactivate/reactivate dance. Use them deliberately (e.g., preserving a video player while it changes parents), not as a default.
Profiling the Three Trees
In practice you observe these trees through DevTools and rebuild markers:
- Flutter Inspector shows the widget tree; toggle to see the render tree with sizes and constraints.
- Rebuild Stats / RepaintRainbow reveal which subtrees re-run
build()or repaint. - Wrapping with
RepaintBoundaryisolates a render-object subtree into its own layer so its repaints don't cascade.
The mental model: too many widgets rebuilding is a reconciliation cost; too many render objects repainting is a raster cost. They are different problems with different fixes.
class IsolatedChart extends StatelessWidget {
const IsolatedChart({super.key, required this.painter});
final CustomPainter painter;
@override
Widget build(BuildContext context) {
// RepaintBoundary gives this CustomPaint its own render layer,
// so frequent repaints here don't dirty the parent's layer.
return RepaintBoundary(
child: CustomPaint(painter: painter),
);
}
}Quick Check: Reconciliation Outcome
A StatefulWidget of type EditorPane with no key is at position 0 of a Column. On the next rebuild, the parent returns a PreviewPane (a different type) at position 0 instead. What happens to the original EditorPane's element and State?
Recap: The Three Trees
You now have a working model of how Flutter reconciles UI:
- Widgets are immutable, throwaway blueprints — cheap to recreate every build.
- Elements are the mutable, long-lived tree that holds
Stateand runs reconciliation viaupdateChild. - RenderObjects are the expensive layout/paint objects Flutter strives to update in place.
- Reuse is decided by
canUpdate: equalruntimeTypeandkeymeans update in place; otherwise the subtree (and its state) is rebuilt. constidentity short-circuits reconciliation;Keys control sibling identity;GlobalKeys move elements across the tree;RepaintBoundaryisolates repaint cost.
With this mental model, performance work becomes precise: minimize unnecessary rebuilds (reconciliation cost) and unnecessary repaints (raster cost) separately.
常见问题解答
「三棵树:Widget、Element 与 RenderObject」课时是免费的吗?
是的 — 「三棵树:Widget、Element 与 RenderObject」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Flutter Mobile Development 课程的其余内容,请升级到 CoddyKit PRO。 Flutter Mobile Development 课程共包含 4 节课。
「三棵树:Widget、Element 与 RenderObject」这节课中我会学到什么?
了解 Flutter 如何在重建过程中协调组件树、元素树和渲染树。 你通过在浏览器中直接运行的动手代码来练习 Flutter Mobile Development,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Flutter Mobile Development 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Flutter Mobile Development 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「三棵树:Widget、Element 与 RenderObject」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Flutter Mobile Development 课中编写并运行代码吗?
能。每节 Flutter Mobile Development 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 三棵树:Widget、Element 与 RenderObject
- 使用 DevTools 时间线分析卡顿
- RepaintBoundary、常量组件与重建裁剪
- 着色器预热与 Impeller 迁移