Why is Flutter Fast?— Part 2: Layered Architecture

Cagatay Ulusoy
Better Programming
Published in
13 min readApr 4, 2022

--

In this series of articles, we aim to answer the question: “Why Flutter is fast?”. According to Adam Barth who is the co-founder of the Flutter project and the author of the first commit in the Flutter repository, Flutter is fast because it is simple.

Adam Barth in his famous Rendering Pipeline talk

In this part, we will explain Flutter’s layered architecture and its impact on performance. For a more detailed overview of the architecture, I would suggest reading the famous blog post from the early days of Flutter: The Layer Cake and the best resource regarding this topic on the Flutter website.

Before Flutter

Knowing the history of Flutter and the problem it is originally meant to solve is very helpful to understand how Flutter works. The best resource that explains the prequel to Flutter is recorded by its co-founders Adam Barth and Eric Seidel [15].

From Webkit to Blink

WebKit is the open-source Web browser engine used by Safari and many other apps on macOS, iOS, and Linux. When Google Chrome is first released in 2008, it used the WebKit engine.

The Apple and Google teams were learning how to operate Webkit together although they were working for companies with competing interests. As Google Chrome evolved, more and more changes were needed in the Webkit and this was slowing down the two teams.

In 2013, Google forked Webkit and started using its own Web browser engine “Blink”. The Blink engine includes DOM, HTML DOM, and CSS rendering engines, Web IDL implementation, Skia graphics engine, and V8 JavaScript engine [17].

Making Web Great for Mobile

The Chrome team knew that they were late to the mobile boom that happened in the late 2000s. Since Chrome started to own its engine, the team began to investigate ways of improving the Web experience on mobile platforms. The approach was stripping away the web platform and only using its essential pieces. The project “Bravo” started in 2013 for this purpose.

The idea of project Bravo was to fetch the Javascript code serialized as JSON over the Internet from a URL and display the content in a self-hosted framework. The Javascript code was run directly to the graphics library, some networking APIs, and platform-specific APIs.

The web platforms use WebGL which is a standard Javascript API for rendering graphics with GPU acceleration. Instead of WebGL, the project Bravo targeted OpenGL for GPU acceleration in mobile devices and took the responsibility of composing its own rendering tree. The team also re-considered the layout models inherited from CSS.

Open the Sky!

The Bravo project ended internally and never went public but the experiments paved the way to Flutter. In 2014, Project Razor was started. The team achieved to shrink Blink by 52%, and was able to reach 19 times faster HTML parsing experience. These promising results led Adam and Eric to leave the Chromium team and start a new project called Sky which is the repository that we know as Flutter today.

In the first days of Sky, the team was deleting thousands of lines of code from Blink. Since they didn’t have any customers that use the Sky framework, they were able to apply the breaking changes fast. However, they were too tied to Javascript and were not able to get responses to their requests from the Chromium team related to Javascript. They started to look for a new language, and they got support from the Dart team. It was a win/win case for both parties, and eventually, the usage of Dart boomed after the adoption of Flutter.

The team found a way of running Dart code in Android using SkyShell. The SkyShell was a universal app that loaded Sky apps over the network. The Standalone SkyShell.apk was used for testing and increased the interest on the Sky project for the internal Google teams.

In 2015, The Sky project scaled fast. In collaboration with the Dart team, they were able to compile the apps for iOS, support Offline mode, remove Sky-Element (DOM) from the framework, and introduce the widget Tree and the render Tree.

Finally, the team decided to rename the project to Flutter which was just a random available domain name that Google owned with acquisitions.

What is Native?

What makes an application native is a controversial question. Can a Flutter application be considered a native application? Adam Barth says yes because the widgets in a Flutter app are built with the same technology that the end developers are using to build their apps [1]. For mobile and desktop, the Flutter application source code is compiled down to the machine code and executed by either a Dart virtual machine or Dart runtime which is shipped with the output file and handles memory allocation and garbage collection.

“Flutter apps are compiled directly to machine code, whether Intel x64 or ARM instructions, or to JavaScript if targeting the web.” [6]

In Android native development, the entire source code is first compiled into the byte code. The Android Runtime (ART) performs the translation of the byte code to machine code at install time so that the devices understand the native instructions. The machine code is executed by the runtime environment of the device [14].

In iOS native development, LLVM is responsible for compiling the source code to machine code. The source code is first compiled into the intermediary language (IR). Then, the LLVM backend generates the machine code for target devices [11].

When developing “native” applications, we draw the UI components on screen using the libraries provided by the platform UI libraries. For example, the button you added to your application may look different depending on the Android API version of the device that the application is installed on.

The look of dialogs depending on Platfiorm API

The platform UI libraries are bridges to the platform rendering APIs such as OpenGL, Metal, Vulkan. These UI libraries are implemented with platform-specific high-level languages, for example, Java, Swift, Objective-C because of portability and safety (memory bugs) reasons. They communicate with the device CPU or GPU to draw pixels on the screen.

As we go lower levels to OS layers, it is almost impossible to use high-level languages due to their characteristics that cause memory bugs. In iOS and Android operating systems, the lower layers are implemented with C, and C++. Recently, Android started support for developing native OS components also in Rust. [10]

taken from [10]

The widget folder provided by the Android platform is a layer built on top of the view folder. Native Android developers should always consider the minimum supported Android API version for their apps when choosing a widget from this folder. Considering how late the Android devices get new API updates, this could have been a serious issue for developers. This problem is solved by including the Android Jetpack libraries (formerly known as support libraries) in the application packages to provide backward compatibility across devices with different Android versions. [7]

Flutter vs. Others

Traditional cross-platform frameworks are wrappers around either OEM widgets or Web view [1]. In the former case, developers build apps using the native UI components with the help of an abstraction layer. For example, React Native developers write code with JavaScript.

React Native’s bridge system is used to provide bidirectional and asynchronous communication between the native side and the Javascript thread. This is a translation process between one high-level language and many high-level languages. Since the UI logic goes from one place to another through an abstraction layer, there might be unexpected performance issues.

Flutter takes a different approach than other cross-platform technologies. The UI is displayed on the screen using its own libraries instead of the platform UI libraries which eliminates one level of abstraction.

Device GPU is utilized with a graphics library shipped with the application. This enables Flutter to provide 60 or 120 frames per second (fps) performance depending on the device’s capability. The Flutter UI toolkit itself doesn’t guarantee this performance. It only provides efficient algorithms that prevent unnecessary build, layout, and paint phases on some widgets in the widget tree. Whenever the device calls that there is a frame to be rendered, Flutter takes this call and starts the rendering process with the build phase. After all, the performance depends on the refresh rate of the target device and how efficiently the application code is implemented to be able to respond fast in the build phase [12].

The framework layer of Flutter bypasses the platform UI libraries by directly communicating with Skia in the engine layer which provides instructions for GPU.

Instead of native UI components, Flutter apps use natively-looking UI components included in the Flutter SDK. Thanks to this approach, the quality and flexibility of the widgets used in Flutter apps are independent of the built-in solutions provided by the underlying platforms.

Developers can use the widgets from the Material Library, Cupertino Library, or fluent_ui package based on the specs from Material Design by Google, Human Interface Guidelines by Apple, and Fluent Design System by Microsoft. Alternatively, developers can also create their own widget set according to their own design language on top of the widgets library.

Layer Model

Flutter has a layered architecture where all the layers are independent, replaceable, and each dependent on the layer below.

Flutter Architectural Layers

Let’s think about the Navigator 2.0 implementation as an example of this layered architectural model. Platforms provide ways of navigation between screens. Mobile platforms utilize gestures, browsers have backward and forward buttons, and some Android phones have a system back button which people have different ideas about how it should work.

According to the layered model, a generic navigation API is provided in the widgets library of the framework layer without knowing which platform the app is used in. The platform-specific implementation details are included in the engine layer.

Let’s start with what we have in the framework layer. Route is an entry managed by the Navigator widget. The Router widget wraps the Navigator widget and configures the navigation history. These classes are implemented in the widgets library of the framework layer.

The CupertinoPageRoute and MaterialPageRoute classes are two concrete implementations of the Route class which are both used to replace the entire screen with a transition. While the former is part of the cupertino library and provides an iOS-style transition, the latter is part of the material library and provides a platform-adaptive transition according to specs in Material Design. These libraries are built on top of the widgets library in the framework layer.

RouteInformation is a data holder that contains information for a route and it is used in the framework layer internally. The Router widget listens to the route updates coming from the platform channels implemented in the engine layer.

When there is a route update coming from the engine, the Router widget instantiates a RouteInformation to be used in the framework layer. Similarly, the underlying platform may need to know about the navigation updates due to user interactions in the app. For example, in Flutter Web apps, the browser address bar may need to be updated when the content changes due to a button press. In this case, the engine gets the necessary information interpreted from a RouteInformation created by the Router widget, and updates its BrowserHistory accordingly.

To sum up, the framework layer depends on the engine layer to build a navigation history. However, it doesn’t need to know about the underlying platform details. For example, pressing the backward/forward buttons in a browser, and receiving an intent from the mobile operating system have different implementations in the engine layer but in the end, the output will be a RouteInformation to be used in the framework layer.

More information regarding the Navigator 2.0 API can be read from my earlier article series.

Engine

The Flutter engine layer is built with C, C++, Java, Objective-C, and Dart languages. Two main pieces in the architecture that are written in C++ are the 2D graphics library and the text rendering. The main reason for keeping these pieces in C++ is that these solutions have already been in use for many years with Skia for 2D graphics, and with Android Open Source Project (AOSP) libraries for text rendering (the text rendering is no longer from AOSP).

Skia is an open-source graphics rendering library maintained by Google. Although Flutter is a young technology, Skia has been around since 2005 and is used by many platforms including Google Chrome, Chrome OS, Android, Mozilla Firefox, and more.

Flutter Engine (Taken from [13])

Skia in Flutter supports various platform-specific backend that generates instructions for the available GPU in the device. For example, Metal for iOS devices was introduced to the Flutter engine with version 1.17.

“Apple’s support for Metal on iOS provides nearly direct access to the underlying GPU and is Apple’s recommended graphics API. On the iOS devices that fully support Metal, Flutter now uses it by default, making your Flutter apps run faster most of the time, increasing rendering speeds by about 50% on average (depending on your workload).” [8]

Building and maintaining a custom engine layer is a complicated task. While the Flutter team explicitly state that they do not object to customizing the engine, they warn about the possible risks due to maintenance cost. The Flutter team doesn’t believe that a custom engine layer can be a long-term solution, and should be avoided as soon as possible, especially if the target platform’s layer is already provided out of box by the Flutter. [9]

AOT Vs JIT Compilation

The Dart code in the release builds of the Flutter apps is directly compiled into native, ARM, and x86 libraries, or Javascript code when the web is targeted. This compilation process is called ahead-of-time (AOT).

The debug builds of Flutter are compiled just-in-time (JIT) and shipped with the Dart virtual machine (VM). This enables injecting new classes, methods, and fields into the running VM. Hot reload is the process of this injection while using the last state of the app [2].

As mentioned in the first article, the Flutter application developer’s responsibility is to describe the state of the UI with a widget tree, and the framework’s responsibility is to update the state with the updateChild method of the elements from top to down in the element tree.

In other words, for developers, everything is a widget, and the framework provides “carefully designed algorithms and data structures to process a large number of widgets efficiently” [4]. Thanks to this separation of concerns the framework is capable of reflecting the changes in the widget tree immediately in the running application with hot-reload.

Hot reload can be thought as editing the CSS in the Web Browser. Although as of today hot reload is still not supported for Flutter Web apps, it works well enough with the rest of the platforms thanks to the declarative paradigm embraced in Flutter [1]. I suggest watching the short video attached below which explains how the hot reload works, and in what cases a hot restart might be needed instead. Kudos to Andrew Fitz for this amazing video with a great visual explanation.

Embedder

“To the underlying operating system, Flutter applications are packaged in the same way as any other native application.” [2]

When we create a new Flutter project the embedder layers for different platforms are provided out of the box by Flutter. Embedder layer uses the engine layer as a library. It is the starting point of a Flutter application when launched. It is written in the platform-specific language and hosts the Flutter engine.

Embedder enables communication with the underlying operating system, obtains threads for UI, and provides texture. The responsibilities of the embedder are lifecycle management, input gestures, window sizing, and platform messages [6].

Entry points to the underlying platform

In the stable channel, we see familiar folder names such as android, ios, web, and windows . These are the folders for the embedded layer. If we want to add embedder for specific platforms, we should explicitly specify them with the command: flutter create --platforms=windows,macos,linux .

Flutter Architecture for Windows

The embedded layer is included in the Flutter SDK, but it doesn’t have to be restricted to these commonly used platforms. Since Flutter architectural layers are replaceable, a platform-specific custom embedder can be integrated into the rest of the layers. We see flutter-elinux led by Sony for embedded Linux devices, flutter-tizen project for porting Flutter to the devices with Tizen OS as example projects.

Flutter for Embedded Linux (eLinux) — Sony

Web applications are sandboxed in a Web Browser application. Therefore, the Flutter Web architecture doesn’t include an embedder that provides communication with the underlying operating system. For example, we can’t import dart.io libraries to Flutter Web projects. Since the engine layer for other platforms contains logic to interface with the underlying operating system, the engine of the Flutter Web applications is the reimplementation of the C++ Flutter engine on top of standard browser APIs.

Conclusion

In this article, we explained the pixel-driven architecture of Flutter rather than relying on the platform widgets. We mentioned how Flutter eliminates one level of abstraction thanks to its own rendering engine library.

In the next article, we will continue answering the question “Why is Flutter fast?” by explaining the simple rendering pipeline of Flutter.

References

  1. Flutter with Tim Sneath and Adam Barth, .NET Rocks! podcast
  2. FAQ, docs.flutter.dev
  3. https://developer.android.com/reference/android/view/View
  4. Inside Flutter
  5. Flutter — The sky’s the limit, Swav Kulinski
  6. Flutter Architectural Overview
  7. AndroidX Overview, developer.android.com
  8. Announcing Flutter 1.17
  9. Custom Flutter Engine embedders
  10. Rust in Android platform
  11. Introduction to LLVM for iOS developer
  12. Software Engineering Radio, Episode 437: Architecture of Flutter
  13. "Flutter: How we're building a UI framework for tomorrow at Google" by Eric Seidel
  14. Advance Android : Android Bytecode Compilation and Build Process +(JVM, DVM, ART, .DEX, .APK)
  15. Before Flutter, Rubber Duck Engineering | Episode 100
  16. Google Chrome Breaks Up With Apple’s WebKit
  17. Blink, Wikipedia

--

--