This article is a summary of a poc I ran this weekend where I used the Closure compiler to bundle a medium sized Svelte application.
Closure Compiler
The closure compiler is an advanced JavaScript optimizer created by Google to reduce the size of JavasScript bundles beyond what’s possible with most mainstream minifiers. While most minifiers primarily rely on name shortening and removal of unused code to reduce bundle sizes, Closure compiler goes further by potentially rewriting and collapsing unoptimized code. Let’s look at an example where Closure compiler makes changes to the code in order to reduce payload size:
In the example above I have created a simple GreetingService with some layered helper methods; getGreetings and createGreeting.
During minification a minifier will try to reduce this further, but conventional minifiers can’t actually reduce this much since there isn’t much to remove in the way of unused code. Let’s look at what we get if we pass this through Rollup/Terser:
As you can see, the original code is left mostly intact, except for shortening the class name and the input parameter to createGreeting.
Let’s compare this to what we get from the Closure compiler below:
As you can see, Closure reduced the entire service to an array of simple objects with a property called “R”. Not only did it cut out all the unnecessary function layers, it also shortened a rather long property name to a single character. As the application grows these types of optimizations will add up and lead to a much smaller bundle size. In general I have often seen a 30-40% decrease in bundle size from using Closure.
This is pretty amazing, but these optimizations can get you into trouble since these aggressive changes make certain assumptions about your code. Example: what happens to a UI view with a reference to the property greetingMsg, which now is known as “R”. Unless you also update all references to greetingMsg your application will be broken.
The only way this works is if Closure also has access to your view code, but this means your html view has to be converted to JavaScript through some form of AoT process.
Svelte is actually a rare example where this seems to work mostly out of the box since the premise of Svelte is to compile your application at build time to plain JavaScript. As a result Svelte will hand the Closure compiler everything it needs to reflect the change in property names on the calling side as well.
Demo
I did my demo by bolting on a rough Closure compiler build process to the default Svelte starter kit. Adding it to the default setup makes it easier to do a side by side comparison. I have uploaded my example to Github in case you are interested in taking a look.
A few years ago I ran a similar experiment with v2 of Svelte. My past experiment worked out relatively well, but it appears v3 is even more compatible with Closure, which made this much easier the second time around. Obviously I am not doing an exhaustive test of Closure compatibility here, but my example exercises a fair bit of Svelte’s functionality.
I was pleased to see how easy it was to get this working with few, if any deviations from standard practices. The only exception is a “hack” where I had to rewrite some internal imports in the underlying Svelte node_modules files. Specifically I ran into an issue with internal module imports internally in the Svelte framework code.
An example of this from store/index.mjs is
Standard node module resolution will resolve ../internal to ../internal/index.[m]js, but Closure seems to have trouble resolving these relative path imports, so I rewrote this to its long form notation ../internal/index.mjs. While redundant, long form notation is still correct for other bundlers, so it doesn’t cause problems with compatibility. This is a simple hack I added using as a sed script (rewrite.sh) in my repo, but there may be more elegant solutions. Otherwise I ran into very few problems integrating Closure with Svelte.
Bundle Size
As expected there was a noticeably difference after adding Closure. The original prod setup resulted in a bundle size of 44.6k, but keep in mind, this includes a decent amount of components as well as external dependencies on rxjs. Bundling it with Closure resulted in a new payload size of 33.4k, which is roughly a 25% decrease. This gap will likely increase as the application grows for the reasons mentioned higher up in the article.
I should point out that these are pre compression numbers. Adding gzip closes the gap to 13.6k vs 12k (11.7%) in Closure’s favor.
I have included a link to the deployed application if you are interested in seeing it run.
You may also be interested in my follow-up article where take a more detailed look at the runtime benefits of the smaller JavaScript payload.