Good day, gentle hackfolk. Like anold-time fiddler I wouldappear to be deep in the groove, playing endless variations on a theme,in this case mobile application frameworks. But one can only recognizenovelty in relation to the familiar, and today's note is a departure: weare going to look at Flutter, a UI toolkit basednot on JavaScript but on the Dart language.

The present, from the past

Where to start, even? The problem is big enough that I'llapproach it from three different angles: from the past, from the top,and from the bottom.

With the other frameworks we looked at, we didn't have to say much abouttheir use of JavaScript. JavaScript is an obvious choice, in 2023 atleast: it is ubiquitous, has high quality implementations, and as alanguage it is quite OK and progressively getting better. Up to now,"always bet on JS" has had an uninterrupted winning streak.

But winning is not the same as unanimity, and Flutter and Dart representan interesting pole of contestation. To understand how we got here, wehave to go back in time. Ten years ago, JavaScript just wasn't a greatlanguage: there were no modules, no async functions, no destructuring,no classes, no extensible iteration, no optional arguments to functions.In addition it was hobbled with a significant degree of what can only becalled accidental sloppiness: with which can dynamically alter alexical scope, direct eval that can define new local variables,Function.caller, and so on. Finally, larger teams were starting tofeel the need for more rigorous language tooling that could use types toprohibit some classes of invalid programs.

All of these problems in JavaScript have been addressed over the lastdecade, mostly successfully. But in 2010 or so if you were a virtualmachine engineer, you might look at JavaScript and think that in someother world, things could be a lot better. That's effectively whathappened: the team that originally built V8 broke off and started towork on what became Dart.

Initially, Dart was targetted for inclusion in the Chrome web browser asan alternate "native" browser language. This didn't work, for variousreasons, but since then Dart grew the Flutter UI toolkit, which hasbreathed new life into the language. And this is a review of Flutter,not a review of Dart, not really anyway; to my eyes, Dart is spirituallyanother JavaScript, different but in the same family. Dart'simplementation has many interesting aspects as well that we'll get intolater on, but all of these differences are incidental: they could justas well be implemented on top of JavaScript, TypeScript, or anothersource language in that family. Even if Flutter isn't strictly part ofthe JavaScript-based mobile application development frameworks that weare comparing, it is valuable to the extent that it shows what ispossible, and in that regard there is much to say.

Flutter, from the top

At its most basic, Flutter is a UI toolkit for Dart. In many ways it islike React. Like React, its interface follows the functional-reactiveparadigm: programmers describe the "what", and Flutter takes care of the"how". Also, like the phenomenon in which new developers can learnReact without really knowing JavaScript, Flutter is the killer app forDart: Flutter developers mostly learn Dart at the same time that theypick up Flutter.

In some other ways, Flutter is the logical progression of React, goingin the same direction but farther along. Whereas React-on-the-web takesthe user's declarative specifications of what the UI should look likeand lowers them into DOM trees, and React Native lowers them toplatform-native UIwidgets,Flutter has its own built-in layout, rasterization, and compositingengine: Flutter draws all the pixels.

This has the predictable challenge that Flutter has to make significantinvestments so that its applications don't feel out-of-place on theirplatform, but on the other hand it opens up a huge space forexperimentation and potential optimization: Flutter has the potential tobeat native at its own game. Recall that with React Native, the resultof the render-commit-mountprocess is a treeof native widgets. The native platform will surely then perform a kindof layout on those widgets, divide them into layers that correspond toGPU textures, paint those layers, then composite them to the screen --basically, what a web engine willdo.

What if we could instead skip the native tree and go directly to thelower GPU layer? That is the promise of Flutter. Flutter has thepotential to create much more smooth and complex animations than theother application development frameworks we have mentioned, with loweroverhead and energy consumption.

In practice... that's always the question, isn't it? Again, pleaseaccept my usual caveat that I am a compilers guy moonlighting in theuser interface domain, but my understanding is that Flutter mostly livesup to its promise, but with one significant qualification which we'llget to in a minute. But before that, let's traverse Flutter from theother direction, coming up from Dart.

Dart, from the bottom

To explain some aspects of Dart I'm going to tell a just-so story thatmay or may not be true. I know and like many of the Dart developers,and we have similar instincts, so it's probably not too far from thetruth.

Let's say you are the team that originally developed V8, and you decideto create a new language. You write a new virtual machine that lookslike V8, taking Dart source code as input and applying advanced adaptivecompilation techniques to get good performance. You can even be fasterthan JS because your language is just a bit more rigid than JavaScriptis: you have traded off expressivity for performance. (Recall from ourdiscussion ofNativeScriptthat expressivity isn't a value judgment: there can be reasons to payfor more "mathematically appealing operational equivalences", inFelleisen's language, in exchange for applying more constraints on alanguage.)

But, you fail to ship the VM in abrowser;what do you do? The project could die; that would be annoying, but youwork for Google, so it happens all the time. However, a few interestingthings happen around the same time that will cause you to pivot. One isa concurrent experiment by Chrome developers to pare the web platformdown to its foundations and rebuild it. This effort will eventuallybecome Flutter; while it was originally based onJS,eventually they will choose to switch toDart.

The second thing that happens is that recently-invented smart phonesbecome ubiquitous. Most people have one, and the two platforms are iOSand Android. Flutter wants to target them. You are looking for yourniche, and you see that mobile application development might be it. Asthe Flutter people continue to experiment, you start to think about whatit would mean to target mobile devices with Dart.

The initial Dart VM was made toJIT, but as we know, Apple doesn'tlet people do this on iOS. So instead you look to write aquick-and-dirty ahead-of-time compiler, based on your JIT compiler thattakes your program as input, parses and baseline-compiles it, andgenerates an image that can be loaded at runtime. It ships on iOS.Funnily enough, it ships on Android too, because AOT compilation allowsyou to avoid some startup costs; forced to choose between peakperformance via JIT and fast startup via AOT, you choose fast startup.

It's a success, you hit your product objectives, and you start to lookfurther to a proper ahead-of-time compiler native code that can standalone without the full Dart run-time. After all, if you have to compileat build-time, you might as well take the time to do some properoptimizations. Youactually change the language to have a sound typingsystemso that the compiler can make program transformations that are valid aslong as it can rely on the program's types.

Fun fact: I am told that the shift to a sound type system actuallystarted before Flutter and thus before AOT, because of aDart-to-JavaScript compiler that you inherited from the time in whichyou thought the web would be the main target. The Dart-to-JS compilerused to be a whole-program compiler; this enabled it to doflow-sensitive type inference, resulting in faster and smaller emittedJavaScript. But whole-program compilation doesn't scale well in termsof compilation time, so Dart-to-JS switched to separate per-modulecompilation. But then you lose lots of types! The way to recover thefast-and-small-emitted-JS property was through a stronger, sound typesystem for Dart.

At this point, you still have your virtual machine, plus yourahead-of-time compiler, plus your Dart-to-JS compiler. Such riches,such bounty! It is not a bad situation to be in, in 2023: you can offera good development experience via the just-in-time compiled virtualmachine. Apparently you can even use the JIT on iOS in developer mode,because attaching ptrace to a binary allows for native codegeneration. Then when you go to deploy, you make a native binary thatincludes everything.

For the web, you also have your nice story, even nicer than withJavaScript in some ways: because the type checker and ahead-of-timecompiler are integrated in Dart, you don't have to worry about WebPackor Vite or minifiers or uglifiers or TypeScript or JSX or Babel or anyof the other things that JavaScript people are used to. Granted, thetradeoff is that innovation is mostly centralized with the Dartmaintainers, but currently Google seems to be investing enough so that'sOK.

Stepping back, this story is not unique to Dart; many of its scenes alsoplayed out in the world of JavaScript over the last 5 or 10 years aswell. Hermes (andQuickJS, for that matter) doesahead-of-time compilation, albeit only to bytecode, and V8's snapshotfacility is a form of native AOT compilation. But the tooling in theJavaScript world is more diffuse than with Dart. With the perspectiveof developing a new JavaScript-based mobile operating system in mind,the advantages that Dart (and thus Flutter) has won over the years arealso on the table for JavaScript to win. Perhaps even TypeScript couldeventually migrate to have a sound type system, over time; it would takea significant investment but the JS ecosystem does evolve, if slowly.

(Full disclosure: while the other articles in this series were writtenwithout input from the authors of the frameworks under review, throughwhat I can only think was good URL guesswork, a draft copy of thisarticle leaked to Flutter developers. Dart hacker Slava Egorov kindlysent me a mail correcting a number of misconceptions I had about Dart'shistory. Fair play on whoever guessed the URL, and many thanks to Slavafor the corrections; any remaining errors are wholly mine, of course!)

Evaluation

So how do we expect Flutter applications to perform? If we were writing a new mobile OS based on JavaScript, what would it mean in terms of performance to adopt a Flutter-like architecture?

Startup latency

Flutter applications are well-positioned to start fast, withahead-of-time compilation. However they have had problems realizingthis potential, withmany users seeing a big stutter when they launch a Flutter app.

To explain this situation, consider the structure of a typical low-endAndroid mobile device: you have a small number of not-terribly-powerfulCPU cores, but attached to the same memory you also have a decent GPUwith many cores. For example, the SoC in the low-end Moto E7Plus has 8CPU cores and 128 GPU cores (texture shader units). You could paintwidget pixels into memory from either the CPU or the GPU, but you'drather do it in the GPU because it has so many more cores: in the timeit takes to compute the color of a single pixel on the CPU, on the GPUyou could do, like, 128 times as many, given that the comparison isoften between multi-threaded rasterization on the GPU versussingle-threaded rasterization on the CPU.

Flutter has always tried to paint on the GPU. Historically it has doneso via a GPU back-end to the Skia graphics library, notably used byChrome among other projects. But, Skia's API is a drawing API, not aGPU API; Skia is the one responsible for configuring the GPU to drawwhat we want. And here's the problem: configuring the GPU takes time.Skia generates shader code at run-time for rasterizing the specificwidgets used by the Flutter programmer. That shader code then needs tobe compiled to the language the GPU driver wants, which looks more likeVulkan orMetal. The process of compilationand linking takes time, potentially seconds, even.

The solution to "too much startup shader compilation" is much like thesolution to "too much startup JavaScript compilation": move this phaseto build time. The newImpeller rendering librarydoes just that. However to do that, it had to change the way thatFlutter renders: instead of having Skia generate specialized shaders atrun-time, Impeller instead lowers the shapes that it draws to a fixedset of primitives, and then renders those primitives using a smaller,fixed set ofshaders.These primitive shaders are pre-compiled at build time and included inthe binary. By switching to this new renderer, Flutter should be ableto avoid startup jank.

Jank

Of all the application development frameworks we have considered, to mymind Flutter is the best positioned to avoid jank. It has theReact-like asynchronous functional layout model, but "closer to themetal"; by skipping the tree of native UI widgets, it can potentiallyspend less time for each frame render.

When you start up a Flutter app on iOS, the shell of the application isactually written in Objective C++. On Android it's the same, exceptthat it's Java. That shell then creates a FlutterView widget and spawnsa new thread to actually run Flutter (and the user's Dart code).Mostly, Flutter runs on its own, rendering frames to the GPU resourcesbacking the FlutterView directly.

If a Flutter app needs to communicate with the platform, it passesmessages across an asynchronous channel back to the mainthread.Although these messages are asynchronous, this is probably the largestpotential source of jank in a Flutter app, outside the initial framepaint: any graphical update which depends on the answer to anasynchronous call may lag.

Peak performance

Dart's type system and ahead-of-time compiler optimize for predictablegood performance rather than the more variable but potentially higherpeak performance that could be provided by just-in-time compilation.

This story should probably serve as a lesson to any future platform.The people that developed the original Dart virtual machine had abuilt-in bias towards just-in-time compilation, because it allows the VMto generate code that is specialized not just to the program but also tothe problem at hand. A given system with ahead-of-time compilation canalways be made to perform better via the addition of a just-in-timecompiler, so the initial focus was on JIT compilation. On iOS of coursethis was not possible, but on Android and other platforms where this wasavailable it was the default deployment model.

However, even Android switched to ahead-of-time compilation instead ofthe JIT model in order to reduce startup latency: doing any machine codegeneration at all at program startup was more work than was needed toget to the first frame. One could add JIT back again on top of AOT butit does not appear to be a high priority.

I would expect that Capacitor could beat Dart in some raw throughputbenchmarks, given that Capacitor's JavaScript implementation can takeadvantage of the platform's native JIT capability. Does it matter,though, as long as you are hitting your frame budget? I do not know.

Aside: An escape hatch to the platform

What happens if you want to embed a web view into a Flutter app?

If you think on the problem for a moment I suspect you will arrive atthe unsatisfactory answer, which is that for better or for worse, atthis point it is too expensive even for Google to make a new web engine.Therefore Flutter will have to embed the native WebView. HoweverFlutter runs on its own threads; the native WebView has its own processand threads but its interface to the app is tied to the main UI thread.

Therefore either you need to make the native WebView (or indeed anyother native widget) render itself to (a region of) Flutter's GPUbacking buffer, or you need to copy the native widget's pixels intotheir own texture and then composite them in Flutter-land. It's not sonice! TheAndroidandiOSplatform view documentation discuss some of the tradeoffs andmitigations.

Aside: For want of a canvas

There is a very funny situation in the React Native world in which, ifthe application programmer wants to draw to a canvas, they have toembed a whole WebView into the React Nativeappand then proxy the canvas calls into theWebView. Flutteris happily able to avoid this problem, because it includes its owndrawing library with a canvas-like API. Of course, Flutter also has theluxury of defining its own set of standard libraries instead ofnecessarily inheriting them from the web, so when and if they want toprovide equivalent but differently-shaped interfaces, they can do so.

Flutter manages to be more expressive than React Native in this case,without losing much in the way of understandability. Few people willhave to reach to the canvas layer, but it is nice to know it is there.

Conclusion

Dart and Flutter are terribly attractive from an engineeringperspective. They offer a delightful API and a high-performance,flexible runtime with a built-in toolchain. Could this experience bebrought to a new mobile operating system as its primary programminginterface, based on JavaScript? React Native is giving it a try, but Ithink there may be room to take things further to own the applicationfrom the program all the way down to the pixels.

Well, that's all from me on Flutter and Dart for the time being. Nextup, a mystery guest; see you then!