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.
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.
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.
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.