Mobile Development 16 min read

Understanding Flutter UI Rendering: Widget, Element, and RenderObject Trees

Flutter renders UI through three interconnected trees—Widget, Element, and RenderObject—where Widgets describe configuration, Elements link Widgets to RenderObjects that perform layout and painting; state changes trigger rebuilds that efficiently update only affected RenderObjects, enabling native‑like performance via the Dart‑Skia pipeline.

NetEase Media Technology Team
NetEase Media Technology Team
NetEase Media Technology Team
Understanding Flutter UI Rendering: Widget, Element, and RenderObject Trees

Since its release, Flutter has attracted increasing attention as a high‑quality cross‑platform technology. Understanding the UI rendering principle and minimizing rendering overhead are essential for building performant applications.

Three Trees

Unlike native UI where an Activity corresponds to a single view tree, a Flutter app maintains three separate trees: the Widget tree, the Element tree, and the RenderObject tree.

In Flutter, almost every object is a Widget . A Widget only describes the configuration of a UI element; it does not represent the actual rendered element.

The class that truly represents the on‑screen element is Element . An Element holds a reference to both its Widget and its RenderObject.

Layout and painting are performed by RenderObject instances.

The relationship is: a Widget creates an Element, which in turn creates a corresponding RenderObject that is attached to Element.renderObject . The RenderObject then carries out layout and painting.

Each Element node corresponds to a Widget object.

A single Widget can correspond to multiple Elements, allowing the same configuration to be instantiated in different locations of the UI tree.

Most Elements have a unique RenderObject, but some (e.g., MultiChildRenderObjectElement ) manage multiple child RenderObjects, forming the overall Render Tree.

In practice, developers usually focus on the Widget tree because the Flutter framework maps Widget operations onto the Element tree, greatly reducing complexity.

Rendering Timing

Flutter triggers a UI refresh in several situations:

When the app starts via runApp() .

When a developer calls setState() on a StatefulWidget .

When an InheritedWidget notifies dependents.

During hot‑reload.

The above actions notify the Flutter framework that its state has changed.

The framework tells the engine to render.

When the next Vsync signal arrives, the UI thread builds a new LayerTree and passes it to the engine.

The engine’s GPU thread composites and rasterizes the layers, finally displaying them on the screen.

Rendering Mechanism

Flutter achieves native‑like performance because its rendering pipeline directly invokes the Skia graphics engine from Dart code, bypassing the extra Java layer present in Android native apps or the JavaScript‑to‑Java bridge used by frameworks like React Native.

Native Android: Java → Skia (C++) → GPU.

Flutter: Dart → Skia (C++) → GPU.

React Native: JavaScript → Android Framework (Java) → Skia → GPU.

Thus, as long as Dart code is as efficient as native Java code, Flutter apps can match native performance. Moreover, Skia is bundled with the Flutter SDK and updates with each SDK release, providing more timely performance improvements than the OS‑bound native Skia.

Flutter Framework Rendering Process

The framework follows a Build → Layout → Paint sequence, analogous to the native Measure → Layout → Draw (Android) or Constraint → Layout → Display (iOS) pipelines. Layout and Paint are handled by RenderObject instances, while the Build step is driven by the Widget/Element hierarchy.

Understanding Element is crucial because it links Widgets to RenderObjects. Elements are divided into two major groups:

ComponentElement (e.g., StatefulElement , StatelessElement ) – they compose other widgets but do not have a RenderObject.

RenderObjectElement (e.g., SingleChildRenderObjectElement , MultiChildRenderObjectElement ) – each holds one or more RenderObjects.

Tree Update Rules

When setState() is called, the framework marks the corresponding element as dirty, adds it to dirtyElements , and later rebuilds it. The rebuild process traverses the previous Element tree, invoking Element.updateChild(child, newWidget, newSlot) for each node.

The Widget tree itself is immutable; a change requires discarding the old tree and constructing a new one. Widgets are lightweight, and Flutter optimizes their creation and destruction, so rebuilding the Widget tree has negligible performance impact. However, rebuilding the RenderObject tree is expensive, so Flutter tries to reuse existing RenderObjects whenever possible.

The core update method Element.updateChild(Element child, Widget newWidget, dynamic newSlot) follows the flowchart shown below:

If newWidget == null and child != null , the child is deactivated and removed.

If newWidget != null and child == null , a new element is created via inflateWidget(newWidget, newSlot) and mounted.

If both are non‑null, the algorithm checks: If child.widget == newWidget , no rebuild is needed. Otherwise, Widget.canUpdate(child.widget, newWidget) determines whether the existing element can be updated. If true, child.update(newWidget) is called. If false, the old child is deactivated and a new element is inflated.

Key helper methods:

Widget.canUpdate(child.widget, newWidget) compares runtimeType and key . Different keys force a new element.

child.update(newWidget) triggers a recursive update of the subtree.

Mounting a new element involves creating the element ( newWidget.createElement() ) and calling mount(this, newSlot) . For RenderObjectElement , mounting also creates the associated RenderObject via createRenderObject and attaches it to the render tree.

Deactivating an element removes it from the render tree and marks it as “inactive”. If it remains inactive after an animation finishes, the framework finally calls unmount , making the element “defunct”.

Example

Consider a simple layout: a Container containing a Row with an Image and a Text widget.

Container(
  child: Row(
    children:
[
      Image.network(''),
      Text("A"),
    ],
  ),
)

The three trees for this layout are illustrated below:

When setState changes the text from "A" to "B", only the RenderObject corresponding to the Text (a RenderParagraph ) needs to be updated. The rest of the tree remains unchanged, demonstrating Flutter's efficient incremental update mechanism.

In real projects, developers isolate frequently changing parts into separate widgets to avoid unnecessary rebuilds and improve performance.

For further performance‑tuning topics, stay tuned to future articles.

Finally, a QR code for downloading the "Gulu Short Video" app is provided below:

Fluttermobile developmentrenderobjectUI renderingElement TreeWidget Tree
NetEase Media Technology Team
Written by

NetEase Media Technology Team

NetEase Media Technology Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.