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
Embedder: This is an extremely lightweight and platform-tailored "shell". On
Android, it is theActivityhosting theFlutterView; oniOS, it is theFlutterViewController. Its job, though unremarkable, is essential: creating the rendering surface, synchronizingvsyncsignals, forwarding touch/keyboard/mouse events, and handling theApplifecycle (backgrounding, rotation, destruction, etc.).Engine: This is the core of the entire system, existing as
libflutter.so(Android) orFlutter.framework(iOS). It hosts theDartruntime (JITin development mode,AOTin release mode), theImpellerrenderer, text layout, image decoding, and the native part of thePlatform Channel.Framework: This is where we usually write code, all in pure
Dart. It is also layered internally:foundationat the bottom, thenAnimation/Painting/Gestures, followed byRendering, thenWidgets, and finallyMaterialandCupertinoat the top. This means most of the "Flutter" logic is actuallyDartsource code that can be read, debugged with breakpoints, and stepped through.
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:
- The tool recompiles only the changed code libraries, generating an incremental
kernel. - This increment is pushed to the running
VMvia theService Protocol. - The
VM"patches" directly in memory, replacing old classes and functions without restarting the app. - 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:
libapp.so: Your application +Framework(native machine code).libflutter.so: TheC++engine.flutter_assets/: Resources like images and fonts.
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):
- Match: Reuse the existing
ElementandRenderObject, only updating the configuration. - Mismatch: Tear down the entire subtree and create a new set.
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:
- Animate: Animation values update.
- Build: Rebuild the
Widgettree marked as "dirty". - Layout: Constraints go down (
Constraints go down), sizes go up (Sizes go up). - Paint: Record drawing instructions.
- Composite: Combine the layer tree into a
Sceneand hand it to theEngine.
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~
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
The three-tree relationship is explained thoroughly.