Three Trees (Widget/Element/RenderObject)
4 min read
Flutter Internals
| Tree | Mutable? | Lifetime | Job |
|---|---|---|---|
| Widget | ❌ Immutable | Often discarded after a build | Describe "what" — config / props |
| Element | ✅ Mutable | Long-lived | Track position in tree; diff old vs new widgets; host State; route inherited lookups |
| RenderObject | ✅ Mutable | Long-lived | Layout, paint, hit testing |
Each widget gets one element (via createElement()), and a RenderObjectWidget also creates one RenderObject (via createRenderObject(context)). Most widgets aren't render objects — Padding, Column, Theme are composition widgets that produce other widgets.
Code in action — what happens on setState
class _CounterState extends State<Counter> {
int _n = 0;
void _inc() => setState(() => _n++);
@override
Widget build(BuildContext context) => Text('$_n'); // NEW Text widget every build
}
Step by step:
setStatemarks this Element dirty.- Next frame, Flutter calls
build()→ returns a new immutableTextwidget. - Flutter walks the Element tree, comparing the new widget to the old one at the same position.
- Same
runtimeType(Text) + key match → reuse the existing Element and update the RenderObject's text. - Different
runtimeType→ unmount old Element, mount new one (and dispose old RenderObject).
The widget object is throwaway; the heavy RenderObject is usually reused. That's the whole performance trick.
Why keys matter — the Element matching rule
// Without keys: Flutter matches by index → state attaches to wrong row when items reorder
Column(children: items.map((i) => CheckboxTile(item: i)).toList());
// With keys: Flutter matches by (runtimeType + key) → correct identity preserved
Column(children: items.map((i) => CheckboxTile(key: ValueKey(i.id), item: i)).toList());
The Element-matching algorithm is the reason keys exist. Without them, the diff is positional; with them, it's by identity.
How the trees interact at runtime
| Action | Widget tree | Element tree | RenderObject tree |
|---|---|---|---|
setState() | New widgets built in subtree | Elements diff, reuse where possible | RenderObjects mutated, NOT recreated |
| New widget type at same position | New widget | Old Element unmounted, new one mounted | Old RenderObject disposed, new created |
GlobalKey move | Same widget | Element moves to new position | RenderObject moves with it |
Theme.of(context) change | n/a | Inherited element notifies dependents | n/a |
Common mistakes to avoid
// ❌ Holding a reference to a widget thinking it persists
// Widgets are recreated each build — your reference goes stale immediately
// ❌ Treating BuildContext as if it's "the widget"
// BuildContext IS the Element. It outlives any given widget instance.
// ❌ Doing heavy work in build()
// build() runs whenever the parent rebuilds. Cache results in State, not in widgets.
// ❌ Confusing `runtimeType` matching with `==` matching
// Flutter compares Type (and Key), NOT equality. Two different Text() widgets with the
// same text are still "same Element" if they're at the same position.
// ❌ Using GlobalKey casually for state lookup
// GlobalKeys are expensive and have lifecycle gotchas — prefer InheritedWidget/Provider
Interview follow-ups
-
Why are widgets immutable? Because they're recreated on every build and the diff must be predictable. Immutability lets Flutter rebuild safely without worrying about widget identity changing mid-frame, and it unlocks
constcanonicalisation — twoconst SizedBox(width: 16)calls return the same instance. -
What's the difference between
runtimeTypeandKeyin Element matching? When Flutter walks the tree comparing old to new, an Element is reused if the new widget has the sameruntimeTypeAND the sameKeyat that position. DifferentruntimeType→ always remount. SameruntimeTypebut different keys → also remount. Same of both → update in place. -
When does an Element become a
RenderObjectElementvs aComponentElement? Render-object widgets (the leaves that actually paint —RenderObjectWidgetsubclasses likeRenderObjectWidget,LeafRenderObjectWidget,SingleChildRenderObjectWidget) getRenderObjectElements. Composition widgets (everythingStatelessWidget/StatefulWidget) getComponentElements, which don't have a RenderObject — they delegate to children. -
How does the State object survive across rebuilds? The
Statelives on theElement, not on the widget. When the parent rebuilds and produces a newStatefulWidgetconfig, Flutter callsstate.updateConfig(newWidget)(effectivelywidget = newWidget) without recreating the State. State only goes away when the Element is unmounted.
How helpful was this content?
Please sign in to rate this article.