Exploration of the Flutter Rendering Mechanism from Architecture to Source Code

By Suce

Flutter is a UI framework that enables the rendering of a set of code on multiple terminals. It adopts a simple rendering pipeline design and provides a rendering engine. Therefore, it provides a better performance and user experience than React Native, Weex, WebView, and other solutions. This article describes the design and implementation of the Flutter rendering mechanism from the architecture and source code perspectives.


Cross-platform technologies show great vitality with improved capabilities resulting from one set of code for multiple terminals. Evolved from hybrid apps, React Native, Weex, applets, and fast apps to Flutter, cross-platform technologies strive to maximize the performance and user experience while improving efficiency. However, all cross-platform technologies face the following challenges:

  • Efficiency: High development efficiency is required on multiple applications, platforms, and containers to ensure one set of code for multiple terminals and business efficiency.

All cross-platform technologies can solve the efficiency issue because efficiency is a basic feature of such technologies. However, it is difficult to solve the performance and user experience issues. The following three solutions are available for rendering:

  • WebView Rendering: When WebView is used for rendering, its features and performance are compromised. For example, PhoneGap, Cordova, and applets use WebView for rendering while other applets use React Native and other rendering solutions at the underlying layer.

Flutter adopts an implementation method similar to the native implementation method with its rendering engine and has excellent rendering performance.

Flutter has development tools, development language, virtual machine (VM), compilation mechanism, thread model, and rendering pipeline. Flutter is like a lightweight operating system (OS), similar to but less resource intensive than Android.

If you are using Flutter for the first time, you can watch the interview entitled, “What is Flutter?” with Eric Seidel, the founder of Flutter. He was previously dedicated to the design and development of the Chromium rendering pipeline. Flutter rendering has some similarities with Chromium rendering, and we will compare them later in this article.

Next, we will describe the design and implementation of the Flutter rendering mechanism from the architecture and source code perspectives. Before that, it would be great to take a look at the Flutter official document “How Flutter Renders Widgets?” This document uses videos, text, and graphics to help you gain a general understanding of the Flutter rendering mechanism.

Architecture Analysis

Architecture Design

In the architecture, the UI thread and GPU thread perform the Flutter rendering.

1. The UI Thread

The UI thread performs steps 1 to 5 in the preceding architecture diagram. Specifically, it executes Dart code (including application code and the Flutter framework code) in the Dart VM. Then, it builds, lays out, and draws the Widget tree, Element tree, and RenderObject tree to generate drawing instructions and generates the Layer tree. The Layer tree stores the drawing instructions.

2. The GPU Thread

The GPU thread performs steps 6 and 7 in the preceding architecture diagram. It runs graphics-related code (Skia) in the Flutter engine, communicates with the GPU to obtain the Layer tree, rasterizes and composites the Layer tree, and displays the tree on the screen.

Note: Flutter uses the Layer tree to organize drawing instructions, which is similar to View DisplayList in Android rendering.

The UI thread functions as the producer, and the GPU thread functions as the consumer.

Process Design

Rendering on Android is driven by VSync signals, and Flutter rendering on Android is no exception. Flutter registers with the Android system and waits for a VSync signal. When Flutter receives the VSync signal, Flutter calls rendering from the Dart framework through the C++ engine and the Java engine. The Dart framework runs Dart code and generates a Layer tree through the Layout, Paint, and other processes. Then, Flutter stores drawing instructions in the Layer tree and posts the rasterization task to the GPU thread. Then, the GPU thread rasterizes and composites the Layer tree and displays it on the screen.

The following figure shows the detailed process:

1. Flutter registers with the Android system and waits for a VSync signal.

When the Flutter engine starts, it registers with an Android system Choreographer and receives the VSync signal callback. A Choreographer is a class for managing VSync signals in Android.

2. After Flutter receives the VSync signal, it calls rendering from the Dart framework through the C++ engine.

After Flutter receives the VSync signal, the Flutter-registered VsyncWaiter::fireCallback() callback is called. Then, Animator::BeginFrame() is executed. Finally, the Window::BeginFrame() method is called. The Windows instance is an important bridge between the underlying engine and the Dart framework. This instance performs most of the platform-related operations, such as input events, rendering operations, and accessibility operations.

3. The Dart framework runs the rendering logic in the UI thread, generates a Layer tree, and posts the rasterization task to the GPU thread.

Then, Window::BeginFrame() is called, and the RenderBinding::drawFrame() method is called in the RenderBinding class. This method relays outs and redraws dirty nodes on the UI, that is, nodes that need to be redrawn. If images are involved during rendering, the images are loaded and decoded in the worker thread and then transferred into the I/O thread to generate image textures. The I/O thread shares the EGL context with the GPU thread. Therefore, the GPU thread can directly access the image textures generated by the I/O thread.

4. After the GPU thread receives the Layer tree, the thread rasterizes and composites the Layer tree and displays it on the screen.

After the Dart framework completes drawing, it generates drawing instructions and stores them in the Layer tree. Then, it calls Animator::RenderFrame() to submit the Layer tree to the GPU thread. Then, the GPU thread rasterizes and composites the Layer tree and displays it on the screen. Finally, the GPU thread calls Animator::RequestFrame() to receive the next VSync signal from the system. This way, the UI can be updated continuously.

Although the call process seems long and complex, it involves few key joints. Therefore, you do not need to focus on the call chain but only need to understand the key implementations. As shown in the following class diagram, we use different colors to mark the key classes. Based on the class diagram, you can determine and understand the Flutter rendering process.

In the preceding class diagram, green cells belong to the Widget, yellow ones belong to the Element, and red ones belong to the RenderObject.

That is all about the overall Flutter rendering process, which involves multiple threads and modules. Next, we will analyze each step in detail. During the preceding architecture analysis, we classified the Flutter rendering process into seven steps. You can also clearly see these steps along the Flutter timeline, as shown in the following figure:

UI Thread

1. Animate

This step is triggered by transientCallbacks of the handleBeginFrame() method. If no animations exist, the callback of this step returns null. If there are animations, Ticker.tick() is called back to trigger the animation widget to update the value of the next frame.

2. Build

This step is triggered by BuildOwner.buildScope(). It is used to build or update the three trees: Widget tree, Element tree, and RenderObject tree.

3. Layout

This step is triggered by PipelineOwner.flushLayout(). Specifically, it calls RenderView.performLayout() to traverse the whole RenderObject tree, calls layout() of each node, updates the RenderObject layout in the dirty area based on information recorded in the Build process. This way, each RenderObject has the correct size and position, which are stored in parentData.

4. Compositing Bits

This step is triggered by PipelineOwner.flushCompositingBits() to update render objects with dirty compositing positions. In this step, each RenderObject determines whether its subitems need to be composited. In the Paint step, this information is used to implement visual effects, such as cropping.

5. Paint

This step is triggered by PipeOwner.flushPaint() and will call RenderView.paint(). Finally, the paint() of each node is triggered to generate a Layer tree, and the drawing instructions are stored in the Layer tree.

6. Submit (Compositing)

This step is triggered by the renderView.compositeFrame() method. This step is officially called Compositing. However, I think it is more appropriate to call it Submit because this step submits the Layer tree to the GPU thread.

GPU Thread

7. Compositing

This step is triggered by Render.compositeFrame(). It builds a scene based on the Layer tree and transmits the scene to the Windows instance for rasterization.

The GPU thread draws a frame of data to the GPU through Skia. The GPU stores the frame information in the frame buffer. Then, it periodically reads frame data from the frame buffer based on VSync signals and submits the data to the monitor for final display.

Rendering Pipeline

When the Flutter engine starts, it registers with an Android system Choreographer and receives VSync signals. After the GPU hardware generates a VSync signal, the system triggers a callback and drives the UI thread to render.

1. Animate

  • Triggering Method: transientCallbacks of the handleBeginFrame() method

Animate is triggered by the transientCallbacks of the handleBeginFrame() method. If no animations exist, the callback returns null. If there are animations, Ticker._tick() is called back to trigger the animation widget to update the value of the next frame.

void handleBeginFrame(Duration rawTimeStamp) {
try {
Timeline.startSync('Animate', arguments: timelineWhitelistArguments);
_schedulerPhase = SchedulerPhase.transientCallbacks;
final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
_transientCallbacks = <int, _FrameCallbackEntry>{};
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
if (! _removedIds.contains(id))
_invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);
} finally {

After handleBeginFrame() ends, handleDrawFrame() is called and triggers the following callbacks:

  • postFrameCallbacks are used to inform the listener that the drawing is completed.

These two callbacks are callback queues in SchedulerBinding.

  • _transientCallbacks are used to store temporary callbacks. It is registered in the Ticker.scheduleTick() to drive animations.

It is used to inform the listener that the processing of the frame is completed. Then, the WidgetBinder.drawFrame() method is called. First, it calls the BuildOwner.buildScope() to trigger the tree update and then performs drawing.

void drawFrame() {
try {
if (renderViewElement ! = null)
} finally {
assert(() {
debugBuildingDirtyElements = false;
return true;

Next, RenderingBinding.drawFrame() is called to trigger the Layout, Paint, and other processes.

void drawFrame() {
assert(renderView ! = null);
if (sendFramesToEngine) {
renderView.compositeFrame(); // Sends the bits to the GPU.
pipelineOwner.flushSemantics(); // Sends the semantics to the OS.
_firstFrameSent = true;

The preceding code is the core process code. Now, let’s take a look at the implementation of the Build process.

2. Build

  • Triggering Method: BuildOwner.buildScope()

As mentioned previously, handleDrawFrame() triggers tree updates. BuildOwner.buildScope() is triggered at the following two time points:

  • Tree Building (during Application Startup): The scheduleAttachRootWidget() method called by the runApp() method builds three trees: Widget tree, Element tree, and RenderObject tree.

The tree building and updating are done by the BuildOwner.buildScope() method. The only difference between tree building and updating is that an element.mo unt(null, null) callback is passed in during tree building. This callback is triggered during the buildScope() process.

This callback builds three trees because a widget is an abstract description of a UI element. We need to inflate the widget into an element and then generate the corresponding render object to drive the rendering.

  • Widget Tree: A widget describes the required configuration of an element. The createElement method is called to create elements and determine whether they need to be updated. Flutter uses the difference algorithm to compare Widget tree changes to determine whether the element status is changed.

3. Layout

  • Triggering Method: PipelineOwner.flushLayout()

Layout is implemented based on one-way data flows. More specifically, the parent node passes constraints to the child node, and the child node passes the size to the parent node. The size data is stored in the parentData variable of the parent node. First, the RenderObject tree is deeply traversed, and then the constraints are recursively traversed. One-way data flows simplify the Layout process and improve the layout performance.

The RenderObject class only provides a set of basic layout protocols and does not define the child node model, coordinate system, and specific layout protocol. Its subclass RenderBox provides a Cartesian coordinate system, which is the same as the Android or iOS native coordinate system. Most RenderObject classes are implemented by inheriting RenderBox. RenderBox has several subclass implementations that correspond to different layout algorithms.

  • RenderFlex is a common flexible layout. It corresponds to the Flex, Row, and Column widgets. For more information about the layout algorithm, please see the code annotations or explanation in this article.

Next, I will introduce two concepts in the layout process: Constraints and RelayoutBoundary.

Constraints, such as BoxConstraints and SliverConstraints, are used by the parent node to limit the size of its child nodes.

RenderBox provides a set of BoxConstraints, which apply the following restrictions:

  • minWidth

We can flexibly implement many common layouts with these BoxConstraints, such as a layout where the child node has the same size as the parent node, a vertical layout where the child node has the same width as the parent node, and a horizontal layout where the child node has the same height as the parent container.

You can calculate the size of a child node with child node size information and these constraints. Then, you need to calculate the position of the child node. The position of a child node is determined by the parent node.

RelayoutBoundary is used for boundary relayout of a child node. When the size of the child node is changed, the parent node does not need to be laid out again. RelayoutBoundary is a flag bit. In the markNeedsLayout() method for marking a dirty area, the flag bit is checked to determine whether relayout is required. The RelayoutBoundary mechanism improves layout performance.

We can obtain the positions and sizes of all nodes after going through the Layout process. Then, we need to draw the nodes.

4. Compositing Bits

  • Triggering Method: PipelineOwner.flushCompositingBits()

After the Layout process ends, the Compositing Bits process is run before the Paint process. This process checks whether render objects need to be redrawn and updates the needCompositing flag of each node in the RenderObject tree. If the value of needCompositing is true, redrawing is required.

5. Paint

  • Triggering Method: PipeOwner.flushPaint()

Related Source Code:

  • Dart-Layer Call Entry: painting.dart

Modern UI systems divide a UI into different graphics layers that can reduce the drawing workload and improve the drawing performance. Therefore, the core concern in the Paint process is to determine the layer where drawing instructions are stored.

The Paint process also uses one-way data flows. First, the RenderObject tree is deeply traversed up down. Then, child nodes are recursively traversed. During the traversal process, the layers and the drawing instructions of each child node to be stored are determined. Finally, a Layer tree is generated.

RepaintBoundary is introduced in the Paint process to improve the drawing performance. RepaintBoundary is similar to RelayoutBoundary in the Layout process.

  • RepaintBoundary redraws a boundary for each child node. When a child node needs to be redrawn, the parent node does not need to be redrawn. RepaintBoundary is a flag bit. In the markNeedsPaint() method for marking a dirty area, this flag bit is checked to determine whether redrawing is required.

The RepaintBoundary mechanism opens the graphics layer division function to developers. Developers can determine which part of a page does not need to be redrawn with this function, such as scrolling containers. This improves the overall performance of the whole page. Redrawing the boundaries changes the final Layer tree structure.

We do not need to redraw all boundaries. Most widgets will automatically redraw boundaries, which are called automatic layering.

When RepaintBoundary is configured for a node, an additional graphics layer is generated and all child nodes of the node are drawn on the new graphics layer. In Flutter, a graphics layer is used to describe all render objects in a drawing instruction buffer. RenderView of the root node will create a root layer that contains several sublayers. Each sublayer contains multiple render objects that form a Layer tree. When a render object is drawn, drawing instructions and parameters are generated and stored at the corresponding layer.

Related layers will inherit the following layer classes:

  • ClipRectLayer is the rectangular cropping layer. You can specify the cropping and rectangular behavioral parameters. Four cropping behaviors are available: none, hardEdge, antiAlias, and antiAliashWithSaveLayer.

For more information about other parameters, please see the preceding Flutter class diagram.

After we learn the basic concepts of drawing, let’s take a look at the drawing process. As mentioned previously, when the first frame is rendered, rendering starts from RenderView of the root node, and then all child nodes are traversed. The drawing process is listed below:

1. Create a canvas object

The canvas object is obtained through PaintContext. In PaintContext, a PictureLayer class is created and called to the C++ layer through ui.PictureRecorder to create a SkPictureRecorder instance. This instance is used to create SkCanvas, and the created SkCanvas is returned to the Dart framework for further use. SkPictureRecorder can be used to record the generated drawing instructions.

2. Perform drawing on the canvas

Drawing instructions are recorded by the SkPictureRecorder

3. Perform rasterization

After the drawing is completed, the Canvas.stopRecordingIfNeeded() method is called, which calls the SkPictureRecorder::endRecording() method at the C++ layer to generate a picture object and store it in PictureLayer. The picture object contains all the drawing instructions. After all of the layers are drawn, a Layer tree is formed.

After drawing is completed, the Layer tree is submitted to the GPU thread.

6. Submit (Compositing)

  • Triggering Method: renderView.compositeFrame()

Note: This step is officially called Compositing. However, I think it is more appropriate to call it Submit because this step submits the Layer tree to the GPU thread.

void compositeFrame() {
Timeline.startSync('Compositing', arguments: timelineArgumentsIndicatingLandmarkEvent);
try {
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
if (automaticSystemUiAdjustment)
assert(() {
if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled)
debugCurrentRepaintColor = debugCurrentRepaintColor.withHue((debugCurrentRepaintColor.hue + 2.0) % 360.0);
return true;
} finally {
  • Create a SceneBuilder object and use SceneBuilder.addPicture() to add pictures generated previously to the SceneBuilder object.

In this process, layers at the Dart framework layer are converted to flow::layer used at the C++ layer. The flow module is a simple Skia-based compositor. It runs on the GPU thread and uploads instructions to Skia. The Flutter engine uses the flow module to cache drawing instructions and pixel information generated in the Paint process. Layers in the Paint process map to the layers in the flow module.

Graphics Pipeline

7. Rasterization and Compositing

With the Layer tree that contains rendering instructions, rasterization and compositing can be performed.

Rasterization converts the drawing instructions into pixel data. Compositing overlays and processes the rasterized data of each layer. This process is called the graphics pipeline.

  • Related Code: rasterizer.cc

Flutter adopts synchronous rasterization.

  • Synchronous Rasterization:

Rasterization and compositing are performed in one thread. Alternatively, thread synchronization or another method is used to ensure the sequence of rasterization and compositing.

  • Direct Rasterization: Drawing instructions in the visible area of DisplayList at a visible layer are executed for rasterization. In the pixel buffer area of the target surface, color values of pixels are generated.

A graphics layer is divided into blocks of the same size based on certain rules. Rasterization is performed by the block. Each rasterization task executes instructions in a block area and writes the execution result to the pixel buffer area of the block. Rasterization and compositing are performed in different threads asynchronously. If a block is not rasterized during compositing, it is left blank, or a checkerboard graphic is drawn.

Android and Flutter adopt the synchronous rasterization strategy and mainly use direct rasterization. Rasterization and compositing are performed simultaneously, and rasterization is completed in the compositing process. Chromium adopts asynchronous block-based rasterization. Graphics layers are divided into different blocks, and rasterization and compositing are performed asynchronously.

Based on the preceding sequence diagram, the rasterization entry is the Rasterizer::DoDraw() method. It will call the ScopedFrame::Raster() method, which is the core implementation of rasterization. This method performs the following tasks:

  • LayerTree::Preroll() makes preparations for drawing.

So far, we have explained the overall rendering implementation of Flutter.

Android, Chromium, and Flutter

Android, Chromium, and Flutter are Google’s star projects. They have similar and different designs in the rendering mechanism. Next, we will compare each one.

The following figure shows the basic design of the modern rendering pipeline:

Let’s take a look at how Android, Chromium, and Flutter are implemented. The following figure shows the Android rendering pipeline:

The following figure shows the Chromium rendering pipeline:

The following figure shows the Flutter rendering pipeline:

The following table compares the three projects:


Lastly, I would like to talk about my understanding of the cross-platform ecosystem. The cross-platform container ecosystem includes at least three layers, as shown in the following figure:

The Frontend Framework Layer

The frontend framework layer directly orients to businesses and requires the following characteristics:

  • Interfaced with the W3C ecosystem

First, it needs to interface with the W3C ecosystem. The W3C ecosystem is (and will be) prosperous and dynamic long-term. If you leave the W3C ecosystem behind and choose to build subsets by yourself, it is difficult to go further.

Second, it must be stable. We need to prevent frontend businesses from being redeveloped when a set of containers is replaced. For example, if we create H5 containers and then applet containers that use different domain-specific languages (DSLs), the frontend team spends a lot of time redeveloping businesses. Although applets support one set of code for multiple terminals, I do not think that this effectively solves the efficiency problem, which contains two major issues:

  • The frontend team has a steep learning curve. The DSL of applets is simple. However, the learning curve of the Flutter widget system is difficult.

If this is the case, it is difficult to run businesses fast. Therefore, no matter how the underlying containers transform, the frontend framework must be constant and stable. However, this stability depends on the unified container layer.

The Unified Container Layer

The unified container layer is between the frontend framework layer and the container layer. It defines the basic capabilities provided by containers, which are relatively stable like protocols.

Protocols are important. For example, with the OpenGL protocol, upper-layer calls do not need to change regardless of how the underlying rendering solution is implemented. For our businesses, we also need to provide common solutions based on the unified container layer:

  • The Unified API Solution

It is inappropriate to redevelop these solutions when a new set of containers are provided. If a solution needs to be redone due to new technologies, the solution is not robust because it does not consider unified or extended cases. Also, the businesses will not wait for us to develop technical solutions with repeated functions.

The Container Layer

Iterations at the container layer aim to maximize the performance and user experience while improving efficiency.

The early React Native mode solves the efficiency problem but introduces a communication layer, which leads to performance issues. React Native implements native rendering by passing the information of the virtual DOM to the native system that builds the corresponding native widget tree based on the layout information. In addition, this translation method needs to adapt to the system version, which incurs additional compatibility issues.

WeChat has launched the applet solution. In my opinion, the applet solution is not a technical solution but a commercial solution because it manages and distributes large traffic of platforms and provides common technical solutions to the businesses. The rendering solutions at the applet underlying layer are also varied.

Flutter solves the performance issue. It builds a GUI system, and the underlying layer directly calls the Skia graphics library for native rendering. This rendering mechanism is the same as Android. However, Flutter chooses the Dart framework due to development efficiency, performance, its own ecosystem, and other factors. This approach abandons the prosperous frontend ecosystem. By looking at the development history of cross-platform containers, embracing the W3C ecosystem as much as possible after solving efficiency and performance issues is probably the best practice for the future. Flutter has also launched Flutter for Web, which aims to support Android and iOS and gradually penetrate the web. We look forward to its performance.

Container technologies are moving forward dynamically. We launched Flutter in 2020 and possibly other technical solutions in 2021. During the solution transition, we need to ensure quick and hitless business migration instead of repeating the same work every time.

WebView performance is also getting better with the improvement of mobile phone performance. Flutter provides a new idea to solve the performance problem. We can look forward to a cross-platform container ecosystem with a complete infrastructure, excellent user experience, and one set of code for multiple terminals.


Related Platform

Related Documents

Related Source Code

Related Sources

Original Source:

Follow me to keep abreast with the latest technology news, industry insights, and developer trends.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store