Problem: During development, it is so easy (and tempting) to add an additional dependency. This can cause your client bundle to bloat, negatively impacting users with constrained network connections (download size) or constrained devices (JavaScript parse times). Dead-code elimination is a good practice but, even with ClojureScript and Google’s Closure compiler, is not enough to eliminate all unnecessary dependencies.

Solution: One defence against client bundle bloating is to continuously monitor the size of the production bundle during development and take action when it grows substantially. Tightening feedback loops allows action to be taken when it is cheaper to do so (source: Lean/Agile!). This post describes an approach to monitor bundle size using the open-source Jenkins continuous integration server.

The basic idea is pretty simple:

Frequently push code changes to version control.

Have a continuous integration server produce a production (optimised) build in the background frequently.

As part of the production build, generate some metrics on bundle size.

Visualise the metrics (or alert, or whatever).

Local Jenkins build automation server

I have a local installation of Jenkins running on my development laptop, although that’s not particularly important for this article. (See my tips for installing on MacOS at the end of the post.)

Local git repositories: working and master

Visualising build size with Jenkins does not require local git repositories, but it works well for me when I don’t wish to use a remote service like Github. The nice thing about having a local file-system master repository is that it is very cheap to do a frequent (every minute) poll of the repository and also works when I’m working offline.

For my applications, I have two local git repositories that I manage. One is my working copy but the other is actually in a cloud-synchronised directory (mainly for off-site backup) and that is the master to which I push my commits. (Jenkins is actually going to create another working copy for the build.)

This is easy to setup as follows, assuming that you already have a local working repository:

# working directory in ~/work/myproject

# master repo to be in ~/cloud-secured/myproject.git git init --bare ~/cloud-secured/myproject.git # In ~/work/myproject

git remote add origin ~/cloud-secured/myproject.git

git push -u origin master

Now, you can possibly get away with pointing Jenkins at your .git directory inside your working directory but I haven’t tried that to know if there are any pitfalls. You could also probably use a post-commit hook but I haven’t had to as the setup above works well for me.

When setting up the build in Jenkins, I simply use a file:// URL, e.g.

file:///Users/<username>/cloud-secured/myproject.git

and set the poll SCM option to be every minute using * * * * * for the cron-like schedule specification.

Simple script to calculate build sizes

In my project, I have a script ./bin/jss-size.sh containing the following:

#!/usr/bin/env bash

export FILE_NAME="./resources/public/js/compiled/app.js" export UNCOMPRESSED_FILE_SIZE=`wc -c < $FILE_NAME`

export GZIPPED_FILE_SIZE=`gzip -c < $FILE_NAME | wc -c` echo $UNCOMPRESSED_FILE_SIZE,$GZIPPED_FILE_SIZE

In this case, the JavaScript bundle is in a file called app.js . We use wc to calculate the number of bytes (the -c option) of a file (piped from STDIN). Using -c for wget will pipe the GZipped output to STDOUT rather than gzipping the file in place. These two metrics are then printed out to STDOUT.

It is probably a good idea to initialise an output CSV file as follows. I have mine simply in the project root directory (laziness!) called js-size.csv .

"JS Size (Uncompressed)", "JS Size (GZipped)"

Jenkins build step to generate metrics

After the main production (optimised!) build step (using webpack , lein , etc.) I have another “execute shell” build step with the following action to simply concatenate the current build’s metrics to a CSV file.

bin/js-size.sh >> js-size.csv

Configuring Jenkins to plot the results

First, install the plot plug-in for Jenkins using the “Manage Plugins” option within the “Manage Jenkins” UI. (I’m using v2.1.0.) Obviously, this only needs to be done once for a Jenkins instance.

In the build configuration, add a “plot build data” post-build action. Specify a plot group (“Statistics”) and title (“Client bundle size”). My y-axis label is “Size (bytes)” and I use a line plot-style with “build descriptions as labels” turned on. I also keep records for deleted builds.

Once you click the button to “Add CSV series,” you can specify the data series file (e.g. js-size.csv ). I also select the option to “display original CSV above plot.”

Production client bundle metrics, finally!

Assuming everything is configured correctly, the next time you run a build, you will get a “Plots” entry for the Jenkins build that might eventually look something like the following:

In the early builds, I stripped down the project to bare minimum, then re-added my dependencies. In the case of this project, I’m using ClojureScript so Google’s Closure compiler is used with advanced optimisations. Simply introducing a library such as Rum (which transitively introduces React) bumps the bundle size up to around 60KB (min+gzip) (see build #18). Around build #29, I introduced clojure.spec and by build #31 I was experimenting with instrumentation. In build #36 I managed to move the instrumentation to only exist in my development builds, so we can see the dependency is removed from production.

Dead-code elimination (e.g. Google Closure compiler’s optimisations) cannot eliminate code that isn’t provably unused. Sometimes simply including a dependency will introduce data or code that the compiler cannot prove isn’t used, so it cannot eliminate it.

Conclusion

Monitoring bundle size in near-real-time during development allows me to be more critical about dependencies I’m introducing. The script can be used outside of the continuous integration server, of course, so it is interesting to just evaluate what weight a library introduces (just by requiring it, without even using it!).