Optimize a Flutter web build
A few weeks ago, I was working on porting my Flutter app to the web. It’s a very straightforward operation. I just needed to add the web
platform, build, and done! Then, I remembered that a lot of people were complaining about Flutter’s web build. The main complaint is that the app (or web app) has to download a huge JavaScript file the first time a user loads the app. This JavaScript file main.dart.js
weighs 2.5MB at least!
Once you start to read this main.dart.js
, we can notice that we can make some optimizations here and there to reduce the total size of the file. I started to have fun trying to find ways to reduce its size. It’s what I’m going to do with you now!
I’ll use Python to write a script to reduce the total size of the main.dart.js
. I’ll compare the final size with the original size and because this file is compressed when transferred, I’ll also compress the final file with gzip
and make a comparison.
Let’s first generate the Flutter project:
flutter create --platforms web -t skeleton my_app
Then, build it for the CanvasKit renderer with the most aggressive JavaScript optimizations.
flutter build web --release --web-renderer canvaskit --dart2js-optimization O4
The current size is 1 871 623
bytes uncompressed and 546 031
compressed.
I’m doing this experiment with the help of regex. Therefore, some optimizations are harder this way than with an AST tree. Some of the optimizations will not produce the exact output but something very close.
Quick Dart Optimizations
Replace true/false with !0/!1
There still are some true
/false
literals in the final code. In JavaScript, !0
/!1
is respectively equivalent to true
/false
.
Remove Indentation
A little part of the code still has indentation. Let’s find that with the regex ^\s+
and replace the matches with empty strings.
Empty Constructor
In JavaScript, if we have an empty constructor like new A()
, we can remove the parentheses.
Number Representations
Use Fractions
Some decimal numbers can be written as fractions. For instance, we could write 1/3 instead of 0.33333333333333… We have to check if the length of the fraction is smaller than the current decimal number otherwise nothing changes.
Decimal Leading Zero
If the integer part of the number is 0, we remove it and only keep the dot and the decimal part. One byte saved!
0.93849
-> .93849
Multiplication by 0
We can find some multiplication by 0. We can remove them peacefully and save a few more bytes.
Shorten the Float Length
It can be risky but let’s try it for the experience. We cut off the decimals after the 8th decimal.
3.14159265359
-> 3.14159265
Use Scientific Notation (Not Implemented)
The idea is to convert numbers multiplied by a power of 10 into the notation eXXX
.
Examples:
5241330000
->524133e4
0.0001
->1e-4
Optimizations That Are Finally Not Optimizations
The following optimizations produce a JS code that is shorter but the gzip output is bigger. After the fact, I realized I was doing the job of a compressing algorithm but in a worse way. For this reason, I do not add them to the optimization chain.
Reduce Null Arguments Array
There’s a lot of functions having plenty of successive null
as arguments like this:
B.Z3 = new A.o(!0, B.A, null, "Roboto", B.E, null, null, null, null, null, null, null, null, null, null, null, B.e, null, null, null, "blackHelsinki displayLarge", null, null, null, null)
The idea is to use the spread operator ...
on an Array of null: ...Array(5).fill(null)
Flip Null Condition
An idea I’ve borrowed from a JS minifier (I guess Babel). They suppose that if(a==null)
could be changed into if(null=a)
and the gzip
algorithm will have better compression. In my case, it makes the compression slightly worse. I would like to retry this one with the AST tree to be sure that my conclusion is correct.
Gather the Literals into Variables
The goal is to capture all the literals (booleans, strings, numbers), count the number of occurrences, and store the most common ones into variables. If we have a lot of 3.14159265359
, we store it into the variable aaa
and replace all the occurrences of 3.14159265359
with aaa
.
Conditions
Remove Some Dead Null Condition
We can find this type of condition s==null?null:s
. It could simply be s
.
Transform x === 0
or x !== 0
Let’s use the fact that 0
combined with the !
operator returns true
and anything else returns false
.
We would rewrite:
x === 0
into!x
x !== 0
into!!x
This optimization is also an approximation and so is not totally correct. The output is not correct JavaScript but the number of chars are equal.
Ternary Condition
There are different micro-optimizations possible for the ternary condition. Let’s do it in this case:
a == null ? 23: a
-> a ?? 23
Nullish Coalescing Assignment Operator ??=
if(a==null) a = 3
could be a ??= 3
Manual Optimizations Linked to Flutter
Those optimizations are probably not producing correct code but I believe it’s a way to drastically reduce the code size.
Flutter provides a system to make the font renderer work for any language. You will have fonts for English-like languages but also for Chinese, Arabic, Japanese, Thai, etc. Your app is probably not translating or displaying these languages. We can try to remove some of them.
We save 15kB when we remove the fallback fonts like Noto Sans Adlam
. Then, we could imagine a smart optimization removing the text theme for the non-English languages. This block alone is about 2.5kB.
B.a_2 = new A.o(!1, null, null, null, null, null, 57, B.B, null, -0.25, null, B.C, 1.12, B.r, null, null, null, null, null, null, "tall displayLarge 2021", null, null, null, null)
B.Z9 = new A.o(!1, null, null, null, null, null, 45, B.B, null, 0, null, B.C, 1.16, B.r, null, null, null, null, null, null, "tall displayMedium 2021", null, null, null, null)
B.YB = new A.o(!1, null, null, null, null, null, 36, B.B, null, 0, null, B.C, 1.22, B.r, null, null, null, null, null, null, "tall displaySmall 2021", null, null, null, null)
B.a_i = new A.o(!1, null, null, null, null, null, 32, B.B, null, 0, null, B.C, 1.25, B.r, null, null, null, null, null, null, "tall headlineLarge 2021", null, null, null, null)
B.a_c = new A.o(!1, null, null, null, null, null, 28, B.B, null, 0, null, B.C, 1.29, B.r, null, null, null, null, null, null, "tall headlineMedium 2021", null, null, null, null)
B.Zq = new A.o(!1, null, null, null, null, null, 24, B.B, null, 0, null, B.C, 1.33, B.r, null, null, null, null, null, null, "tall headlineSmall 2021", null, null, null, null)
B.Xe = new A.o(!1, null, null, null, null, null, 22, B.B, null, 0, null, B.C, 1.27, B.r, null, null, null, null, null, null, "tall titleLarge 2021", null, null, null, null)
B.a_3 = new A.o(!1, null, null, null, null, null, 16, B.ad, null, 0.15, null, B.C, 1.5, B.r, null, null, null, null, null, null, "tall titleMedium 2021", null, null, null, null)
B.XT = new A.o(!1, null, null, null, null, null, 14, B.ad, null, 0.1, null, B.C, 1.43, B.r, null, null, null, null,
null, null, "tall titleSmall 2021", null, null, null, null)
B.Yt = new A.o(!1, null, null, null, null, null, 12, B.ad, null, 0.4, null, B.C, 1.5, B.r, null, null, null, null, null, null, "tall labelLarge 2021", null, null, null, null)
B.a_y = new A.o(!1, null, null, null, null, null, 11, B.ad, null, 0.5, null, B.C, 1.45, B.r, null, null, null, null, null, null, "tall labelMedium 2021", null, null, null, null)
B.Zk = new A.o(!1, null, null, null, null, null, 10, B.ad, null, 0.5, null, B.C, 1.4, B.r, null, null, null, null, null, null, "tall labelSmall 2021", null, null, null, null)
B.Wu = new A.o(!1, null, null, null, null, null, 16, B.F, null, null, null, B.C, 1.75, B.w, null, null, null, null, null, null, "tall bodyLarge 2021", null, null, null, null)
B.Ym = new A.o(!1, null, null, null, null, null, 14, B.F, null, null, null, B.C, 1.5, B.w, null, null, null, null, null, null, "tall bodyMedium 2021", null, null, null, null)
B.a_w = new A.o(!1, null, null, null, null, null, 12, B.F, null, null, null, B.C, 1.45, B.w, null, null, null, null, null, null, "tall bodySmall 2021", null, null, null, null)
Removing it and a few other styles we can gain about 50kB.
Conclusion
By running all the optimizations, we can gain about 10kB in total. I believe more advanced optimizations could be achieved if the logic were to be done with an AST parser instead of regex. It’s also easy to break the code with regex because the JavaScript syntax is quite tricky.
That was a fun experiment that probably inspired me to do something similar but with the help of an AST tree and ESBuild. The experiment ends here and I hope you learned something today.