Optimizing Wasm and JS module size and load time.
Recently Iāve thought of a static search for this blog without the need to run a backend.
So I started to build the following āsearch engineā:
I donāt know until now if I will deploy this anytime, but itās a good project to make use of a little bit functional programming, Rust and Wasm.
However, one problem I faced was the size of the Wasm module and JS overhead for the search.
You can see in the figure that there are at least 2 JS components and 1 Wasm module. The index is stored in a simple JSON file. The whole index of the current blog has a size of 2MB which leads to a loading time of ~50 seconds on āSlow 3Gā!
A long loading time is crashing the idea of static sites IMHO. Furthermore it is not so good to let visitors load the whole index even if they donāt use the search. Eating up their bandwidth is not my goal.
Thatās why I started to explore how much load time and size I can save via optimization.
1. Optimizing .wasm size
One step towards a better load time, is the optimization of the size of the Wasm module. See also: Shrinking .wasm code size. This document presents some parameters which can be changed to shrink the size, as Wasm is usually optimized for speed rather than size!
Here are the three changed parameters and their outcome.
All load times are measured with a disabled cache and the āslow 3Gā param in Chrome DevTools.
1.1. Link Time Optimizations (LTO):
Setting the following in Cargo.toml:
[profile.release]
lto = true
Had a huge effect on size change!
| Parameter | Before | After | Percentage change |
|---|---|---|---|
| Module size | 149kB | 115kB | -22.81% |
| Load time | 5.00s | 4.31s | -13.8% |
1.2. Optimize for Size (instead speed):
In addition to the parameters in 1.1. the opt-level can be set to s:
[profile.release]
opt-level = 's'
| Parameter | Before | After | Percentage change |
|---|---|---|---|
| Module size | 115kB | 106kB | -7.82% |
| Load time | 4.31s | 4.13s | -4.17% |
Thereās also the z paramter. But the documentation says that it is not always shrinking the size in comparison to s and a measurement is needed. Which was true in this case:
[profile.release]
opt-level = 'z'
| Parameter | Before | After | Percentage change |
|---|---|---|---|
| Module size | 106kB | 127kB | +19.81% |
| Load time | 4.13s | 4.55s | +10.16% |
1.3. Using the wasm-opt tool
The wasm-opt tool is also able to change the size:
| Parameter | Before | After | Percentage change |
|---|---|---|---|
| Module size | 106kB | 102kB | -3.7% |
| Load time | 4.13s | 4.12s | -0.24% |
Unfortunately not so much.
Saved overall
Overall the decrease is pretty noticeable. Optimizing the module size is worth the small amount of work.
Module size: -31.54%
Load time: -17.6%
2. Optimizing loading of index
The even bigger part of the search is the index, which will also grow with the content. With 2MB and ~42 seconds of load time, it is eating the bandwidth.
Here are some actions and ideas to shrink the size and load time.
2.1. Split the index
The first step is obvious: Split the index and load only the parts that are needed.
In the case of my index it was easy: Every letter is stored in its own file. When the use starts typing, the indices get loaded lazily.
This has a huge effect: One of the biggest indices is the one for the letter c. This will be taken for the comparison.
| Parameter | Before | After | Percentage change |
|---|---|---|---|
| Module size (letter ācā) | 2.0MB | 153kB | -92.35% |
| Load time | 42.09s | 5.10s | -87.88% |
The disadvantage is that the user will wait a small amount of time every time he starts to type in a new, not yet loaded, letter.
You can see the effect here:
2.2. Load only once
With the right techniques and caching it will help a lot to load the index only once. So that it is reusable even for a revisit of the page. But this will lead to some other challenges.
2.3. Serializing the index
Another technique to shrink the size of a document is to serialize it.
Using the avro schema led to an incredible decrease of -23.52% without losing any data!
| Parameter | Before | After | Percentage change |
|---|---|---|---|
| Module size (letter ācā) | 153kB | 117kB | -23.52% |
But the problem is, that the deserialization logic and package, either in Rust or JS was ~500KB in size. I didnāt do the math, but I think this doesnāt pay off for such small indices. I abandoned the idea.
3. Smaller js files
There are two JS files for the search: The interface to the Wasm module (search.js) and the glue code between the input field and the search interface (glue.js).
This can also be optimized:
search.js:
| Parameter | Before | After | Percentage change |
|---|---|---|---|
| Module size | 16.1kB | 9.3kB | -42.23% |
| Load time | 2.33s | 2.22s | -4.72% |
glue.js:
| Parameter | Before | After | Percentage change |
|---|---|---|---|
| Module size | 2.5kB | 1.5kB | -40% |
| Load time | 2.05s | 2.04s | -0.48% |
Notice the huge change of module size, but the small change of the load time.
4. Some other ideas
Beside of this low hanging fruits and the usage of parameters to change the size, there are some other possibilities to change the size and load time:
- Smaller HTML files (will need an optimization for the whole blog).
- Enabling brotli compression (which is not supported by my hosting services).
- Using another data structure for the index like a Trie.
Conclusion
There are some easy ways to optimize the load times and size of Wasm and JS modules. Sometimes you just need to set some parameters to get a better user experience. Here I explored some of them and I could feel the change in the User Experience.