Unless you've worked on frontend at Google at some point in the past 20 years, it's unlikely that you've ever encountered the Closure Compiler. It occupied a similar niche to TypeScript, but TypeScript has absolutely, definitively won.
Still, it's interesting to revisit CC for a few reasons:
- By looking at a system that made different high-level design decisions than TypeScript, we can gain a deeper appreciation of TypeScript's design.
- It shows us missing features from TypeScript that it might not have even occurred to us to want.
If you've ever used TypeScript with
Compare this TypeScript:
An invalid invocation of
max will result in an error:
This is similar to what
tsc does in some ways but different in others. Just like
tsc, it reports type errors in your code. And just like
There are some interesting differences, too. The Closure Compiler reports that our code is "100.0% typed". Using TypeScript terminology, this is a measure of how many
any types you have. (Effective TypeScript discusses using the type-coverage tool to get this information in Item 44: Track Your Type Coverage to Prevent Regressions in Type Safety.)
To see how this works, let's look at some code to fetch and process data from the network.
Here's an "externs" file (the CC equivalent of a type declarations file) that defines a type and declares a function:
Some interesting things to note here:
- Types are introduced via
@typedefin a JSDoc comment. The
- The declaration of
fetchDataincludes an empty implementation. TypeScript would use
declare functionhere, but this is not JS syntax. So CC uses an empty function body.
Here's some more code that fetches data and processes it:
<script> tag (CC predates Node.js). No build step is required and your iteration cycle is very tight.
Let's look at what happens when you compile this:
Here's what that looks like when we unminify it:
Just like TypeScript, compilation here mostly consists of stripping out type information (in this case JSDoc comments).
Now look at what happens when we turn on "advanced optimizations":
The output is much shorter. Here's what it looks like unminified:
This is a radical transformation of our original code. In addition to mangling our variable names (
a), the Closure Compiler has mangled property names on
g) and inlined the call to
processData, which let it remove that function entirely.
The results are dramatic. Whereas the minified code with simple optimizations was 231 bytes, the code with advanced optimizations is only 62 bytes!
Notice that CC has preserved some symbols: the
fetchData function and the
bar property names. The rule is that symbols in an "externs" file are externally visible and cannot be changed, whereas the symbols elsewhere are internal and can be mangled or inlined as CC sees fit.
This is great stuff! So why didn't the Closure Compiler take off?
The externs file was critical to correct minification. Without it, CC would have mangled the
fetchData function name and the
bar properties, too, which would have resulted in runtime errors. Omitting a symbol from an externs file would result in incorrect runtime behavior that could be extremely difficult to track down. In other words, this was a really bad developer experience (DX).
CC introduced some extralinguistic conventions to deal with this. For example, in JS (and TS) there's no distinction between using dot notation and square braces to access a property on an object:
This is not true with the Closure Compiler. Its convention is that quoted property access is preserved whereas dotted can be mangled. Here's how that code comes through the minifier with advanced optimizations:
There's another big problem with advanced optimizations: in order to consistently mangle a property name, CC needs to have access to all the source code that might use it. For this to be maximally effective, all the code you import must also be written with the Closure Compiler in mind, as must all the code that that code imports, etc.
In the context of npm in 2023, this would be impossible. In most projects, at least 90+% of the lines of code are third-party. For this style of minification to be effective, all of that code would have to be written with the Closure Compiler in mind and compiled by it as a unit.
On the other hand at Google in 2004, or 2012, or perhaps even today, that is quite realistic. At huge companies, the first- to third-party code ratio tends to be flipped. Using third-party code is more painful because there are legal and security concerns that come with it, as well as a loss of control. TypeScript's zero runtime dependencies are a good example of this.
Contrast this with TypeScript. It only needs to know about the types of existing libraries. This is all that's needed for type checking. The DefinitelyTyped project has been a monumental undertaking but it does mean that, generally speaking, you can get TypeScript types for almost any JS library. (There's a similar, though much smaller, set of externs to get type checking for popular JS libraries for the Closure Compiler.)
goog.require provided a module system and
goog.require might inject a
There were a few problems with this. One was that all the
import 'react', not "facebook/react".
This transition happened early in TypeScript's history, but late in the Closure Compiler's. Presumably adaptation was harder.
tsc is written in TypeScript) and distributed with npm.
TypeScript also won by focusing more on developer tooling. The Closure Compiler is an offline system: you run a command, it checks your program for errors, then you edit and repeat. I'm not aware of any standard Closure language service. There's no equivalent of inspecting a symbol in your editor to see what CC thinks its type is. TypeScript, on the other hand, places as much emphasis on
tsc. Especially with Visual Studio Code, which is written in TypeScript and came out in 2015, TypeScript is a joy to use. TypeScript uses types to make you more productive whereas Closure used them to point out your mistakes. No wonder developers preferred TypeScript!
(Google engineers are no exception to this. In the past decade they've adopted TypeScript and migrated to it en masse. You can read about one team's experience porting Chrome Devtools from Closure to TypeScript).
goog namespacing reinforced this.
--checkJs. But using JSDoc for all types is awkward and noisy. Ergonomics do matter and TypeScript's are undeniably better.
There's a general principle here. I'm reminded of Michael Feathers's 2009 blog post 10 Papers Every Developer Should Read at Least Twice which discusses D.L. Parnas's classic 1972 paper "On the criteria to be used in decomposing systems into modules":
Another thing I really like in the paper is his comment on the KWIC system which he used as an example. He mentioned that it would take a good programmer a week or two to code. Today, it would take practically no time at all. Thumbs up for improved skills and better tools. We have made progress.
The KWIC system basically sorts a text file. So are we correct to laud our progress as software developers? This would be a one-liner today:
But think about what makes this possible:
- We're assuming that the entire file fits in memory, which almost certainly would not have been true in 1972.
- We're using a garbage collected language, which would have been a rarity back then.
- We have an enormous library at our fingertips via node built-ins and npm.
- We have great text editors and operating systems.
- We have the web and StackOverflow: no need to consult a reference manual!
All of these things are thanks to advances in hardware. The hardware people give us extra transistors and the software people take most of those for ourselves to get a nicer development process. So it is with faster network speeds and the Closure Compiler. We've taken back some of that bandwidth in exchange for a more flexible development process and ecosystem.
There were discussions of adding minification to TypeScript in the early days but now optimized output is an explicit non-goal for the language. If you've ever thought that type-driven minification would be a beautiful thing, the Closure Compiler is a fascinating data point. It can be tremendously effective, but it also comes at an enormous cost to the ecosystem.
There's a lively discussion of this article on Hacker News. In particular Paul Buchheit (the creator of Gmail!) points out that runtime performance was very much a goal of the Closure Compiler and inlining/dead code removal was a way to achieve this. It's hard to get back in the pre-JIT IE6 mindset where every getter comes with a cost! I don't think this changes the conclusions of the article. Also, the Closure Compiler is not the Google Web Toolkit (GWT).