fbpx

Using Closure Compiler With Webpack + Typescript via Tsickle

blog
Using Closure Compiler With Webpack + Typescript via Tsickle
Using Closure Compiler With Webpack + Typescript via Tsickle

Using Closure Compiler With Webpack + Typescript via Tsickle

 

Written by Nick Jacob

At AppMonet we serve hundreds of terabytes of javascript each day to tens of millions of devices. Our SDK is deployed in hundreds of top apps; sometimes in multiple apps on the same phones. It’s critical that we minimize our the impact of our SDK — ads should consume as few resources as possible.

Javascript bloat plagues the open web; the average webpage has 350kb of gzipped Javascript (Addy Osmani has an awesome talk about why JS file size is so problematic on mobile). Because AppMonet is mobile-only, we’re especially sensitive to the size of our Javascript files—besides lower network speeds, we’re also running in resource-constrained devices with slow javascript runtimes (Android pre-lollipop, iOS UIWebview).

Closure Compiler ADVANCED_OPTIMIZATIONS

The closure-compiler used to require running a jar (wrapped by a node module)—it’s now also available as a native binary or pure-javascript library, giving much better compile times.

In a lot of cases, closure compiler is similar in performance to uglifyJS/terser—especially if you’re able to mangle properties in uglifyjs. However, there is a special mode—ADVANCED_OPTIMIZATIONS that is really the holy grail of javascript optimization. From the closure compile docs:

The ADVANCED_OPTIMIZATIONS level compresses JavaScript well beyond what is possible with other tools.

Closure Compiler Compilation Levels | Google Developers

The Closure Compiler lets you choose from three levels of compilation, ranging from simple removal of whitespace and comments to aggressive code transformations. The WHITESPACE_ONLY compilation level removes comments from your code and also removes line breaks, unnecessary spaces, extraneous punctuation (such as parentheses and semicolons), and other whitespace.

Unfortunately using it isn’t so easy—if you read the closure compiler docs about ADVANCED_OPTIMIZATIONS, you’ll see that you not only have to carefully declare all of your “externs” (anything that can’t be mangled beyond recognition), but alsoannotate all of your code with jsdoc-like comments to achieve maximal performance.

These comments are a very es3-centric way of expressing types:


/**
 * A shape.
 * @interface
 */
function Shape() {};
Shape.prototype.draw = function() {};

/**
 * @constructor
 * @implements {Shape}
 */
function Square() {};
Square.prototype.draw = function() {
  ...
};

But if we’re using typescript, doesn’t our code already contain all of this information about types and visibility? If we could convert typescript to this annotated javascript, we’d be able to use ADVANCED_OPTIMIZATIONS without any changes to our code!

Luckily, the angular team has developed a tool called tsickle, which does exactly that! From the tsickle github:


* inserts closure-compatible JSDoc annotations on functions/classes/etc
* converts ES6 modules into goog.module modules
* generates externs.js from TypeScript d.ts (and declare, see below)
* declares types for class member variables
* translates export * from ... into a form Closure accepts
* converts TypeScript enums into a form Closure accepts
* reprocesses all jsdoc to strip Closure-invalid tags

Awesome! Unfortunately, tsickle isn’t exactly mainstream:

We already use tsickle within Google to minify our apps (including those using Angular), but we have less experience using tsickle with the various JavaScript builds that are seen outside of Google.

How can we use tsickle to transform typescript in our existing webpack (and/or) rollup based projects? The tsickle readme mentions that it’s a drop-in replacement for tsc, but does that mean we can use it directly with ts-loader? Luckily Inseok Lee has provided an example webpack loader as a github gist.

This loader is using the typescript compiler to load and process the tsconfig.json file, and then runs tsickle.emitWithTsickle to transform typescript. In our webpack config, we’ll replace ts-loader with this localtsickle-loader. This will produce an annotated javascript source:

You won’t actually see this annotated JS—the loader passes it through to the the rest of the webpack build process (e.g., the next loader in the chain).

This means that we can pass the tsickle-annotated javascript closure compiler in the webpack optimization stage:

Note that we use ModuleConcatenationPlugin; this is just to explicitly enable tree-shaking: closure compiler will perform advanced dead-code elimination, and if you output ES5 modules from it, webpack can reduce its bundling overhead via tree-shaking.

To use this setup in production, we wanted to add some tests to this loader and cover some edge cases in tsickle compilation:

  • handling multiple typescript module types (es5 output is optimal for tree-shaking)
  • allow passing options to specify the tsconfig.json and the location of the externs.js file
  • fix some issues in tsickle output (caused by using it in a webpack loader)

We’ve open sourced our implementation of tsickle-loader:

AppMonet/tsickle-loader

This is a webpack loader for tsickle; it lets us compile typescript code with the typescript compilaer, while adding annotations & externs readable by closure compiler, which means we can use ADVANCED_OPTIMIZATIONS mode. See the directory for an example of compiling this way.

Used with google-closure-compiler and the relatively newclosure-webpack-plugin, we can build some very optimized javascript. Given this arbitrary example:

closure-compiler outputs 1,333 bytes — the webpack terser default produces 1,466 bytes. These numbers aren’t that useful, since most of that output is the webpack bundler API. However you can see the closure-compiler output is able to mangle the class properties in a way that will help reduce bundle size as the code’s complexity grows.

Here’s the closure compiler output beautified so it’s easier to read:

Just 10kb of javascript reduced across 100 million SDK sessions will reduce bandwidth by 1TB. More importantly, it will help reduce resource usage across millions of phones and improve user experience.

You can see that tsickle+closure compiler correctly preserved the structure of the PublicAPI interface.

When you combine this with typescript’s module: "es2015" and use webpack’s tree-shaking (or, port the tsickle-loader to rollup), you can really reduce the impact of using utility libraries or 3rd-party code in your library—widely repeated imports are well-mangled, dead code is eliminated, and the remaining code is heavily optimized.

The closure compiler will also include any necessary polyfills based on your actual code usage and the target output language: es5 array methods are included individually, so you aren’t shipping a shim for findIndex if you only ever use find.

In some tests, we see smaller bundle sizes as the application gets larger:

Using Closure Compiler With Webpack + Typescript via Tsickle

Just 10kb of javascript reduced across 100 million SDK sessions will reduce bandwidth by 1TB. More importantly, it will help reduce resource usage across millions of phones and improve user experience.

Drive revenue and boost engagement. Find out how.