세 가지 트리: Widget, Element 및 RenderObject
다시 빌드하는 동안 Flutter가 위젯, 요소 및 렌더 트리를 어떻게 조정하는지 이해합니다.
세 가지 트리: Widget, Element 및 RenderObject은(는) CoddyKit의 무료 Flutter Mobile Development 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 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” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Flutter Mobile Development 강의 전체를 잠금 해제할 수 있습니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.
“세 가지 트리: Widget, Element 및 RenderObject”에서 뭘 배우나요?
다시 빌드하는 동안 Flutter가 위젯, 요소 및 렌더 트리를 어떻게 조정하는지 이해합니다. 브라우저에서 직접 실행하는 실습 코드로 Flutter Mobile Development을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Flutter Mobile Development을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Flutter Mobile Development은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.
“세 가지 트리: Widget, Element 및 RenderObject” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Flutter Mobile Development 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Flutter Mobile Development 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 세 가지 트리: Widget, Element 및 RenderObject
- DevTools 타임라인으로 버벅거림 프로파일링
- RepaintBoundary, 상수 위젯 및 다시 빌드 가지치기
- 셰이더 예열 및 Impeller 마이그레이션