Dart Web Client Programming Head to Head: Flutter, Jaspr, and Dart Web

A big part of the Dart Snake project was so that I could evaluate the experiences of using Dart for server side development. A year ago I looked at the REST server landscape with this post and how Dart compared to Go in that respect . The Dart Snake project instead let me look at how well Dart worked for the front end by doing three different implementations for the web: Flutter Web, Jaspr, and plain old Dart Web Standard Libraries. In this post I compare these technologies with respect to the development experience and performance. If you’d like to play any of these versions to compare directly you can find the links below:

Development Experience

The development experience for the three platforms really falls into two categories: do you want to do web programming or are you trying to reuse your mobile/desktop application in a web app deployment. Flutter Web is very much not about writing web apps in the traditional sense. The code you are writing is identical to the code you would use on your mobile/desktop app. There is no exposure to Document Object Model (DOM) elements. There is no concept of browser-side calls. All of that is abstracted away so that it feels like writing code for any other Flutter target platform. The plus side is that that means that publishing for the web is pretty much just changing the target of your flutter buildor flutter run command, assuming that the libraries you are using supports the web platform.

For people that need to or want to develop against the DOM and using traditional web development principles there is the support built into the standard Dart libraries, which I refer to in this post as “Dart Web”. This is because Dart started as a technology to replace JavaScript for writing websites. That legacy has never gone anywhere even if it has been overshadowed by its use for writing applications in Flutter. This is just as low level as writing in plain old modern JavaScript without any framework (React, Vue, Svelt, Angular, etc). This is where Jaspr comes in. There has been some effort to get some JavaScript frameworks ported over to Dart like the AngularDart and the VueDart . These never really hit critical mass even within the Dart community. In the case of the VueDart. They also very much borrow the exact same idioms and patterns as their JavaScript counterparts. Jaspr on the other hand is about trying to make the experience feel like Flutter since that is where most Dart development is happening right now.

Jaspr borrows the idioms and patterns of Flutter but applies them against DOM manipulation rather than building Widgets. The promise is being able to reuse a lot of the services/business logic of your application and keep many of the design patterns as well. Both of these are built around the Declarative UI versus Imperative UI concept. In Flutter you have a large tree of StatelessWidgets and/or StatefulWidgets. In Jaspr you have a tree of StatelessComponents and/or StatefulComponents. In Flutter each of these widgets has a build method that returns a Widget when it is told to redraw. In Jaspr each of these components has a build method that returns a stream of components (slightly different but essentially the same). In both cases the framework, often helped along with a state management system like Riverpod , will decide whether a particular widget/component needs to be redrawn.

We saw a taste of this in our Snake implementations. Take for example a GameBoard widget in Flutter:

class GameBoardWidget extends StatelessWidget {
  final GameBoard board;

  const GameBoardWidget({super.key, required this.board});

  @override
  Widget build(BuildContext context) {
    const tileSize = 10.0;
    return SizedBox(
      width: board.width * tileSize,
      height: board.height * tileSize,
      child: GridView.count(
        crossAxisCount: board.width,
        children: board.tiles
            .map((t) => TileWidget(
                tileSize: tileSize, 
                position: t.position,
            ))
            .toList(),
      ),
    );
  }
}

…versus the same in Jaspr:

class GameBoardWidget extends StatelessComponent {
  final GameBoard board;

  const GameBoardWidget({super.key, required this.board});

  @override
  Iterable<Component> build(BuildContext context) sync* {
    const tileSize = 10.0;
    yield DomComponent(
      tag: 'div',
      styles: gridStyles,
      children: gameBoard.tiles
          .map((t) => TileWidget(
            tileSize: tileSize, 
            position: t.position,
           ))
          .toList(),
    );
  }
}

You can see some syntactical difference in the way the objects that compose our UI are returned and the types that are returned. You would also find that there isn’t a corresponding Jaspr component library to go along with the giant Flutter Widget library. However the way the components are assembled and returned will feel very natural for Flutter developers. This therefore gives developers a Flutter-like framework for doing their web-app development using standard DOM manipulation.

For tooling/building these different technologies have their own command line tools. Flutter Web’s is just the usual Flutter command line system that you use for mobile/desktop. For Dart Web there is the webdev tool. Jaspr has its own jaspr CLI tool but can also use the webdev tool as well. At the time of this writing the Jaspr CLI’s build command seems to not be generating Minified code though. This makes being able to invoke the build with the webdev tool helpful when it is time to finally deploy.

Each of these tools one can invoke these engines that allow for hot reloading/refreshing of the app as development is underway to make seeing changes near-realtime. Modern browsers also have extensions to allow one to directly see the Dart code as it is executing client side to do in-browser debugging of the application at development time. This makes the development process for all of these very similar to what would be available for JavaScript development.

Performance

Let’s look at the performance of the various framework options. Performance is driven a lot by the size of the web app’s files that are being downloaded by users and processed by their JavaScript engines. It isn’t always true that smaller is better but substantial differences in size can impede both download time and the time to get initial operation of your application. First, let’s look at what the build deployments file sets look like for each implementation using our Dart Snake implementations again. For this I’m using the fully Minified Jaspr code from the webdev build not the un-Minified versions from the Jaspr CLI.

Both Dart Web and Jaspr reduce the entire payload for the web app into just four files: index.html, style.css, main.dart.js, and a favicon. All of these are served by the web server that the app is served from. We manually wrote the index.html and style.css for these tools. The main.dart.js file is a compilation of the various Dart code of our application across all of the libraries into one Minified main.dart.js file.

The Flutter Web version has a bit more going on. It does have an auto-generated index.html file for hosting the app. It too has a main.dart.js file which encapsulates all of the dart code in our application and its dependency libraries. It then has a bunch of other files as well. First there is some Flutter runtime code, which exists in the flutter.js file. Next there are the fonts that it uses. The definitions and one of the fonts comes from our server but the Roboto font is being downloaded from fonts.gstatic.com. Finally there is the CanvasKit engine that Flutter users for rendering. This is comprised of a canvaskit.js file and a canvaskit.wasm file, both served up from unpk.com. All files except those explicitly mentioned were hosted by the server that you are hosting the web app from.

# Files Total Size (KB) Transfer Size (KB)
Dart Web 4 116.77 117.64
Jaspr 4 273.05 273.95
Flutter Web 10 10,977.28 6,615.04

In terms of total size of the payload being downloaded, Dart Web comes in smallest with around 117 KB of size. Jaspr comes in a bit beefier at 273 KB. Flutter on the other hand has a total size of 10.75 MB. With compression on transfer the total data transfer for the Flutter app is a bit over 6.5 MB. A lot of that extra size is in needing all the CanvasKit stuff. The application sizes, as measured by the size of the main.dart.js though is also dramatically larger for Flutter Web. Dart Web’s is about 116 KB. Jaspr’s is about 272 KB. As you can see these are what is driving most of the size of these directories. Flutter’s main.dart.js size on the other hand is 1.92 MB.

main.dart.js (KB) Other Dart JS (KB) WASM (KB)
Dart Web 115.54 0.00 0.00
Jaspr 272.29 0.00 0.00
Flutter Web 1,966.08 141.02 7,014.40

Again though, smaller isn’t necessarily better so lets look at some user facing time metrics. First, there is the download time. For these timings the files were hosted on this blog’s Digital Ocean server not behind a CDN. I accessed them from a Firefox browser running on an x64 i7-6700K workstation running Linux Mint 20 which is connected to the internet via bi-directional 1 GB fiber. The data center and my workstation are about 1500 km (930 mi) apart. Along with download speed I wanted to measure the time after download is completed that the application is usable. Performance tools often measure “time to first draw” which is the time before the site is more than just a blank screen. That is important but for this I want time to first interactivity as defined as the time before the snake head and food icons are first drawn on the screen.

Load Time (sec) Post-Loading Startup Time (sec)
Dart Web 0.75 0.79
Jaspr 0.88 1.10
Flutter Web 1.69 2.90

The loading time is obviously scaling with the size of the bundles. Because a browser caches these files this penalty should only be incurred every once in a while. The time between when the payload finishes getting delivered and the game is playing is a more enduring metric. The app essentially has to setup the game state, draw 1600 tiles, and setup callbacks for each of these tiles at a bare minimum. Once done the game draws and starts stepping forward in time. Dart Web, essentially pure JavaScript now, does this in about 0.79 seconds. Jaspr, which has to set up some additional framework things as well as drawing these elements does this in 1.10 seconds, so about a third of a second longer. Flutter has to bootstrap the CanvasKit system that it is using for doing all its drawing, as well as all the same object initialization etc. It completes this in 2.90 seconds. Any of these times are within the realm of usable but it is clear that there is a big overhead for Flutter Web.

Conclusion

Looking at these three different development options for writing web apps it is clear that one could use any of them for this purpose. Dart Web is the lightest but is essentially like coding a site in JavaScript without any framework. Flutter Web is the heaviest and more importantly is really about deploying a Flutter app to a web target not doing traditional web programming. Jaspr’s overhead is very close to Dart Web but provides a Flutter-like framework to build more complex web apps. If I were trying to port my Flutter app to the web as a nice to have additional deployment target then the choice of Flutter Web is a no-brainer. If however I was looking to develop a native web app in Dart then I’d start my search with Jaspr.