跪拜 Guibai
← Back to the summary

Flutter's Three-Tree Architecture and the Impeller Rendering Pipeline, Explained

Welcome to follow the WeChat public account: FSA Full Stack Action 👋

I. Core Architecture: Flutter's Foundation Through a Three-Layer Model

Most cross-platform frameworks solve the problem of "how to control two platforms with one set of code" in a similar way: they try to remotely control the platform's native UI components through a "shell".

But Flutter takes a completely opposite path. It doesn't use the platform's built-in UI components at all. Your App, the Flutter framework, and the entire Widget library are all Dart code, which in release mode is compiled directly into native ARM or x64 machine code. This code drives Flutter's own C++ engine, which then draws every frame directly onto the GPU through the Impeller renderer.

This decision explains almost all of Flutter's characteristics: why the UI looks identical on different devices; why Hot Reload can hot-update code while preserving the application state; why performance is so stable; and why making it look like a "native" app requires extra effort. In Flutter's eyes, the platform provides not buttons or lists, but merely a window, a GPU surface, and a stream of input events.

1. Three-Layer Architecture Diagram

To the operating system, a Flutter application is actually quite "boring"—it's just a full-screen native view within an ordinary application process. The truly interesting mechanisms are all hidden inside the view, divided into three layers:

Layer Implementation Language Core Responsibilities
Framework Dart Provides Material / Cupertino design languages, Widget library, Rendering, Animation, etc.
Engine C++ Impeller renderer, Dart runtime, text layout, frame scheduling, Platform Channel pipeline
Embedder Platform-specific Android (Activity), iOS (ViewController), desktop window management, vsync signals, input events

2. Detailed Roles of Each Layer

II. Compilation and Execution: Why Hot Reload Exists?

Flutter runs Dart in completely different ways depending on the compilation mode. Understanding this explains why Hot Reload is so smooth and why release-mode performance is so strong.

1. Debug Mode: An Embedded Virtual Machine Inside the App

When you run flutter run, the Flutter tool parses packages, generates a plugin registry, and then calls Gradle or Xcode to build the shell. Your Dart source code is compiled into an intermediate representation format called kernel (stored in .dill files), which is then loaded by the Dart VM inside the Engine and JIT (Just-In-Time) compiled.

This is the principle behind Hot Reload:

  1. The tool recompiles only the changed code libraries, generating an incremental kernel.
  2. This increment is pushed to the running VM via the Service Protocol.
  3. The VM "patches" directly in memory, replacing old classes and functions without restarting the app.
  4. The framework calls reassemble(), marking components as "dirty" and triggering a rebuild.

The key point is: The Element Tree and State objects are not discarded; only a Diff is performed between the old and new configurations. This is why your navigation stack, text in input fields, and counter values are preserved.

2. Release Mode: Abandon the Virtual Machine, Go Straight to Machine Code

flutter build apk or ipa takes a different path. The gen_snapshot tool AOT (Ahead-Of-Time) compiles your App and Framework together into native machine code, simultaneously removing unused code (Tree-shaking).

In an Android APK, you will ultimately see:

There is no JIT, no interpreter, and no runtime reflection here, which is why dart:mirrors cannot be used in Flutter.

III. Rendering Pipeline: From Widget to GPU Pixels

This is the place most likely to confuse newcomers: why doesn't "rebuilding Widgets every frame" cause lag?

1. The Collaboration of Three Trees

In Flutter, buttons, padding, themes, and even gesture detection are essentially composable Dart objects. But remember: A Widget is not a view. It has no size and is not responsible for drawing; it is just a temporary "blueprint".

The real work is done by two other trees:

Tree Type Characteristics Responsibilities
Widget Tree Immutable, rebuilt often Provides the description (blueprint) of the UI
Element Tree Mutable, persistent Manages State, responsible for connecting Widgets and RenderObjects
Render Tree Expensive, mostly reused Responsible for layout (Layout), painting (Paint), and hit-testing (Hit-test)

When you call setState(), the Widget Tree produces a bunch of new Widget objects. The Framework compares these new Widgets against the existing Element Tree (checking if runtimeType and key match via canUpdate):

So rebuilding is not scary, because the expensive RenderObject remains almost untouched.

2. Rendering Pipeline: The Birth of a Frame

When a User taps the screen, the Embedder forwards the event, the Engine finds the corresponding RenderObject and triggers a callback (like onTap). Then setState() is called, marking the Element as "dirty" and requesting a new frame from the Engine.

When the next vsync signal arrives, the UI thread executes the following phases in order:

  1. Animate: Animation values update.
  2. Build: Rebuild the Widget tree marked as "dirty".
  3. Layout: Constraints go down (Constraints go down), sizes go up (Sizes go up).
  4. Paint: Record drawing instructions.
  5. Composite: Combine the layer tree into a Scene and hand it to the Engine.

There is a very important principle here: Constraints go down, sizes go up. This is why putting a ListView inside a Column throws an "unbounded height" error—because the Column provides infinite constraints, and the ListView tries to occupy infinite height, causing a conflict.

3. From Rendering to Pixels: The Significance of Impeller

Previously, Flutter used the Skia engine, which dynamically compiled Shaders at runtime. This meant that the first time a specific animation ran, noticeable frame drops (Jank) could occur due to Shader compilation.

Now Flutter uses Impeller by default. Its approach is direct: no more runtime compilation; instead, the required Shaders are pre-compiled when the engine is built. This way, the rendering thread just needs to hand instructions to Metal (iOS) or Vulkan (Android), fundamentally solving the problem of first-frame animation stutter.

IV. Summary and Trade-offs

Flutter's architecture gives developers great freedom, but it is not without cost.

1. Comparison of Advantages and Disadvantages

Dimension Flutter Approach Native Approach
UI Consistency Extremely high, pixel-perfect reproduction with one codebase Depends on implementation differences of platform components
Performance Ceiling High and predictable, no communication bridge overhead Highest, direct calls to system capabilities
Package Size Larger (needs to bundle its own engine) Smaller
Native Interaction Requires bridging via Channel or FFI Natively supported

2. Final Words

Understanding this architecture means you will no longer wonder why Widget rebuilding is so fast, nor be surprised why Hot Reload can preserve state.

The essence of Flutter is a reactive framework with its own rendering engine. It skips the system's UI logic and takes control of the pixels itself. This "I want it all" boldness, while incurring the cost of a larger package size, also brings something extremely precious in cross-platform development—a highly consistent development experience.

If the article was helpful to you, please don't hesitate to click and follow my WeChat public account: FSA Full Stack Action, this will be the greatest encouragement for me. The public account has not only Android technology, but also articles on iOS, Python, etc., which may have the skill points you want to learn about~

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

亚雷

The three-tree relationship is explained thoroughly.