Optimizing performance in Flutter web apps with tree shaking and deferred loading

Per Classon
Flutter
Published in
4 min readMay 19, 2020

For the best user experience it is important that an app loads fast. The initial load time of a Flutter web application can be improved by minimizing its JavaScript bundle. The Dart compiler includes features such as tree shaking and deferred loading, both of which minimize the JavaScript bundle. This article explains how they work and how you can use them in your application.

Tree shaking by default

When compiling a Flutter web application, the JavaScript bundle is generated by the dart2js compiler. A release build has the highest level of optimization, which includes tree shaking your code.

Tree shaking is the process of eliminating dead code, by only including code that is guaranteed to be executed. This means that you do not need to worry about the size of your app’s included libraries because unused classes or functions are excluded from the compiled JavaScript bundle.

To see tree shaking in action:

  1. Create a Dart file greeter.dart:
abstract class Greeter {
String greet(String name);
}
class EnglishGreeter implements Greeter {
String greet(String name) => 'Hello $name!';
}
class SwedishGreeter implements Greeter {
String greet(String name) => 'Hej $name!';
}
void main() {
print(EnglishGreeter().greet('World'));
}

2. Run dart2js -O4 greeter.dart in your terminal and take a look at the generated output out.js.

In the generated JavaScript code, there aren’t any references to the SwedishGreeter class, or any inclusion of the string Hej $name, as it was removed during tree shaking by the compiler.

The compiler can only figure out what code is reachable, and what is dead code, with static analysis. Take the following example, where the greeter is defined depending on the system locale:

Locale locale = Localizations.localeOf(context);
if (locale.languageCode == 'sv') {
greeter = SwedishGreeter();
} else {
greeter = EnglishGreeter();
}

The compiler doesn’t know the user’s system locale, therefore both EnglishGreeter and SwedishGreeter are included in the JavaScript bundle. For such use cases deferred loading can help in minimizing the initial bundle size.

Only load code when needed with deferred loading

Deferred loading, also called lazy loading, allows you to load libraries if and when needed. It can be used to load rarely-used functionality of an application. Please note that deferred loading is a dart2js feature, so this is not available for Flutter mobile applications. In the simplest case, mark an imported package or file as deferred and wait for it to load before using it:

import 'greeter.dart' deferred as greeter;void main() async {
await greeter.loadLibrary();
runApp(App(title: greeter.EnglishGreeter().greet('World')));
}

Compiling this code generates two JavaScript files. When loadLibrary is called on the deferred import, it loads the greeter library.

In Flutter, where everything is a widget, you might want to make use of the FutureBuilder. A widget’s build method is expected to be synchronous, therefore you can’t call await on loadLibrary inside of a build method. However, you can return a FutureBuilder in a build method, and you can also use it to show a different UI while the library is loading:

import 'greeter.dart' deferred as greeter;FutureBuilder(
future: greeter.loadLibrary(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text(greeter.greet('World'));
} else {
return Text('Loading...');
}
},
)

To try it yourself (see a full example on GitHub), open Chrome DevTools and click the Network tab to inspect network activity. Reload the page to see when the library is loaded and imported. In the following screenshot, loading the main.dart.js_1.part.js file is deferred:

Deferred loading of localizations in the Flutter Gallery

The Flutter Gallery supports over 70 languages, but most users only use one. Deferring the loading of the localization strings is a great use of this feature. For example, after implementing deferred loading of localization strings in Flutter Gallery, the app’s initial JavaScript bundle size was cut in half. If you have a lot of localization strings in your Flutter web application, consider deferring the loading of those files. The gen_l10n.dart script includes the flag --use-deferred-loading for this purpose (currently only available on the 1.19 SDK master channel).

This post is a part of a series about what we learned when improving performance for the Flutter Gallery. I hope you found it useful and that you learned something that you can apply to your Flutter web application!

This post is a part of a series about what we learned when improving performance for the Flutter Gallery. Articles in the Creating performant Flutter web apps series:

--

--