8th January 2024
Today, I aimed to enhance the initial loading speed of the Leporello.js app.
Leporello.js employs ES6 modules without any build steps - the entire content of the repository is uploaded directly to Github Pages. It's straightforward! However, this approach might impede the app's initial loading speed.
Typically, an application features a root module, often named index.js
, acting as the entry point. We include this module in our index.html
using the script
tag:
<script type='module' src='index.js'></script>
index.js
imports various other modules:
import * as Foo from 'foo.js';
import * as Bar from 'bar.js';
import * as Baz from 'baz.js';
However, a challenge arises here. The browser lacks prior knowledge that index.js
will import foo.js
, bar.js
, and baz.js
until it downloads and parses index.js
. Consequently, concurrent downloading of index.js
, foo.js
, and bar.js
isn't feasible. Additionally, foo.js
confronts a similar predicament, as it also imports other modules that cannot be downloaded until foo.js
is parsed.
Until recently, bundlers were the primary solution. Bundlers concatenate multiple ES6 modules into a single file. Yet, bundlers introduce several drawbacks:
Thankfully, over the past year, all major browsers have begun supporting an alternative to accelerate website load times: module preloading.
How does it function? By adding the following line to your index.html
:
<link rel="modulepreload" href="foo.js" />
This informs the browser that our website will utilize a module with the source 'foo.js.' Hence, the browser can speed up loading by fetching the file in advance. A simple build step can generate the link rel=modulepreload
tag for each module in our src
directory.
I created a basic bash script that operates without dependencies:
PRELOADS=""
for f in `find src -name '*.js'`; do
PRELOADS=$PRELOADS"<link rel='modulepreload' href='/$f'>"
done
sed -i "s#.*PRELOADS_PLACEHOLDER.*#$PRELOADS#" index.html
This script lists all .js
files in the src
directory, generates a link
tag for each file, and inserts the resulting text into index.html
, replacing a designated placeholder - a simple HTML comment containing the text PRELOADS_PLACEHOLDER
:
<!--PRELOADS_PLACEHOLDER-->
Voila!
Now, let's turn to benchmarking.
Initially, I benchmarked the version without module preloading and bundling.
Using Github Pages as hosting and Chrome Dev Tools with network throttling (Slow 3G) enabled, the app loaded in 17.3 seconds.
Refer to the waterfall chart:
The chart showcases a ladder-shaped pattern, a symptom elucidated earlier.
Upon introducing module preloading, the app loaded in 10.6 seconds -a substantial acceleration!
The subsequent waterfall chart reveals a distinct pattern - no longer a ladder, but a flat list. All modules download simultaneously:
Let's compare module preloading to bundling. Bundling modules with Rollup yielded a result of 9.8 seconds. A minor improvement over module preloading.
The waterfall chart notably shrinks, now displaying one substantial bundle file as opposed to numerous small, separate module files:
As an advocate of the nobuild approach, I'm enthused about module preloading. Coupled with import maps, they facilitate the development and publication of JS apps without convoluted toolchains.