Why Babel Matters

Why Babel is different from other compile-to-JS systems like CoffeeScript and TypeScript, and how it's going to become the driving force for innovation in JavaScript.

@author Charles Pick
@date May 18, 2015

Babel is a transpiler for JavaScript best known for its ability to turn ES6 (the next version of JavaScript) into code that runs in your browser (or on your server) today. For example the following ES6 code:

const input = [1, 2, 3];
console.log(input.map(item => item + 1)); // [2, 3, 4]

is compiled by Babel to:

var input = [1, 2, 3];
console.log(input.map(function (item) {
  return item + 1;
})); // [2, 3, 4]

Which runs in just about any JavaScript environment.

Babel was created by an extremely dedicated Australian developer called Sebastian McKenzie who has worked tirelessly to ensure that Babel can handle all of the new syntax that ES6 brings, along with builtin support for React’s JSX extensions and Flow type annotations. (Babel also makes use of an excellent set of polyfills called core-js, written by a talented Russian software engineer called Denis Pushkarev, and is built upon the very fast acorn JavaScript parser by Marijn Haverbeke and Ingvar Stepanyan.)

Of all the ES6 transpilers, Babel has the greatest level of compatibility with the ES6 spec, even exceeding the much longer established Traceur by Google.

Babel lets you use virtually all of the new features that ES6 brings today, without sacrificing backwards compatibility for older browsers. It also has first class support for dozens of different build & test systems which makes integration with your current toolchain very simple.

If we stopped here, Babel would already be pretty compelling, unless perhaps you’re already using an alternative compile-to-JS language, like CoffeeScript or TypeScript.

In fact, Babel is often compared to CoffeeScript. They are both transpilers after all, and since CoffeeScript influenced the design of ES6, they have a lot in common in terms of syntax (minus of course things like significant whitespace).

Typescript brings other benefits, like type safety and a whole host of other language features, but it’s not quite JavaScript and doesn’t play nicely with technologies like JSX. If you use TypeScript, you can’t really use anything other than TypeScript, and if you do, you lose many of the benefits that TypeScript provides, e.g. type safety.

If you’re already using CS or TS you might be tempted to stick with what you already know, however, the similarities between them are quite superficial, where CoffeeScript and TypeScript are merely languages, Babel is much more than that, fundamentally Babel is a platform.

Babel: The ESNext Proving Ground

Babel doesn’t only track ES6, it also tracks the next version of JavaScript - ES7 (or ES2016). Amongst other things this brings support for async/await, two keywords that are already radically changing the way people write asynchronous JavaScript. They allow developers to write greatly simplified code which is far easier to debug and reason about than when using callbacks, or dealing directly with promises. For example, recursively reading a directory using callbacks is pretty awkward (code from Stackoverflow):

function walk (dir, visitor, done) {

  // this flag will indicate if an error occured (in this case we don't want to go on walking the tree)
  let dead = false;

  // this flag will store the number of pending async operations
  let pending = 0;

  function fail (err) {
    if (!dead) {
      dead = true;
      done(err);
    }
  };

  function checkSuccess () {
    if (!dead && pending == 0) {
      done();
    }
  };

  function callVisitor (file, stat) {
    if (!dead) {
      try {
        visitor(file, stat);
      } catch (error) {
        fail(error);
      }
    }
  };

  // this function will recursively explore one directory in the context defined by the variables above
  function dive (dir) {
    pending++; // async operation starting after this line
    fs.readdir(dir, (err, list) => {
      if (!dead) { // if we are already dead, we don't do anything
        if (err) {
          fail(err); // if an error occured, let's fail
        } else { // iterate over the files
          list.forEach(file => {
            if (!dead) { // if we are already dead, we don't do anything
              const path = dir + "/" + file;
              pending++; // async operation starting after this line
              fs.stat(path, (err, stat) => {
                if (!dead) { // if we are already dead, we don't do anything
                  if (err) {
                    fail(err); // if an error occurred, let's fail
                  } else {
                    if (stat && stat.isDirectory()) {
                      dive(path); // it's a directory, let's explore recursively
                    } else {
                      callVisitor(path, stat); // it's not a directory, just call the visitor
                    }
                    pending--;
                    checkSuccess(); // async operation complete
                  }
                }
              });
            }
          });
          pending--;
          checkSuccess(); // async operation complete
        }
      }
    });
  };

  // start exploration
  dive(dir);
};

(If you’ve got a more succinct callback version, feel free to email us.)

But compare the above to the following equivalent using async/await:

Note: Presumes a promisifed fs module.

async function walk (dir, visitor) {
  const filenames = await fs.readdirAsync(dir);
  await Promise.all(filenames.map(async _filename => {
    const filename = path.join(dir, _filename);
    const stat = await fs.statAsync(filename);
    if (stat && stat.isDirectory()) {
      await walk(filename, visitor);
    } else {
      visitor(filename, stat);
    }
  }));
}

It’s so much more succinct and simple to follow that it’s obvious that this will become the way to write asynchronous JavaScript, and Babel allows you to use it today, even though mainstream browser adoption is several years away at this point.

As Babel aligns closely with TC39 (the technical committee that leads the design and development of JavaScript), it is in the unique position of being able to provide real-world-usable implementations of new ECMAScript features before they are standardized.

To take another example, Kevin Smith (@zenparsing) has been recently working on a Function Bind proposal that allows functions to be bound to their context declaratively, without having to use Function.prototype.bind().

E.g.

fetch("someUrl").then(::console.log);

compiles to

fetch("someUrl").then(console.log.bind(console));

and also allows method call chaining whilst maintaining the original context.

Since Babel has added experimental support for this feature, developers have been able to try it and test it in their applications. This provides essential real-world feedback about the feature design, which means that any edge cases or shortcomings can be found before the spec is finalized. This feedback is going to be absolutely crucial to the ongoing successful evolution of JavaScript.

Babel: The Platform

Babel’s plugin system is its secret weapon, and this is where the ecosystem around Babel will really take off. Babel allows custom code transformers to be plugged in to the compilation process. These code transformers take an AST (Abstract Syntax Tree, a collection of nested objects which describes the structure of your program) and perform manipulations upon it before it is turned into executable JavaScript.

These manipulations can take many forms, at codemix we’ve been experimenting with adding static & runtime type checks using flow type annotations (another awesome feature supported by Babel!), automatic closure hoisting & elimination for faster, more memory efficient code and even hygienic JavaScript macros.

Babel provides scope tracking, traversing, variable renaming and a whole host of other helper functions that make writing complicated AST transformations much easier.

Babel: For Faster JavaScript

One trade-off that all JavaScript engines must make is that of compilation time vs optimization time. On the web it’s utterly vital that JavaScript files are loaded and executed quickly, otherwise the user experience really suffers. The JIT simply does not have time to perform all of the optimizations that are technically possible at runtime, and therefore it must make tradeoffs, avoiding certain operations that would yield faster code, but are too costly to perform. It’s also unable to perform many optimizations because of JavaScript’s dynamic type system. Instead it must perform speculative optimizations but give itself the option to bail out back to slower, safer code if some of its assumptions turn out to be false. These “deopts” are extremely expensive and can radically harm application performance.

Being run at compile time, Babel does not have quite such intensive time constraints. Because of its impressive scope tracking and type inference features, it’s the ideal base upon which to build transformers which perform these kinds of optimizations. We’ve started the ball rolling with closure-elimination which turns closures into normal functions where possible, and hoists them up to the highest possible scope when not.

Babel already supports some kinds of optimization out of the box, e.g. it can automatically hoist immutable JSX higher up the scope chain to avoid unnecessary allocations, and it performs Constant Folding / Constant Propagation via the utility.deadCodeElimination transformer. Constant Folding takes an expression, e.g. 1 + 2 + 3 and replaces it with the result of the expression - 6. Constant propagation takes a variable that never changes (may or may not be declared as const) and inlines it wherever it is referenced (although this only works for primitive values like strings and numbers).

Here’s some other ideas for Babel plugins that we’d expect to see over the next few months:

Arbitrary function inlining

Inlines functions into their callsites for maximum performance and to avoid polymorphism and the overhead of a function call. While most JITs perform this operation themselves, it’s not always possible, particularly if the function exceeds certain complexity heuristics. With Babel, we can perform this kind of transformation at compile time, resulting in potentially much faster code:

function add (a, b) {
  return a + b;
}
function demo () {
  return add(1, 2);
}
console.log(demo());

should be transformed to:

function demo () {
  return 1 + 2;
}
console.log(demo());

which would then get further simplified to:

function demo () {
  return 3;
}
console.log(demo());

and finally

console.log(3);

Function Duplication

Polymorphism is the cause of many slow downs in JavaScript. Where a function cannot be inlined, it should be made a candidate for duplication to ensure functions remain monomorphic (fast!). Via type inference + annotations, it detects the types of arguments passed to a function and duplicates the function based on those types. This is hard to explain, so here’s an example:

function add (a, b) {
  return a + b;
}
function foo () {
  return add("foo", "bar");
}
function bar () {
  return add(1, 2);
}
function baz () {
  return add("foo", 123);
}

should be transformed to:

function add_string_string (a, b) {
  return a + b;
}
function add_number_number (a, b) {
  return a + b;
}
function add_string_number (a, b) {
  return a + b;
}
function foo () {
  return add_string_string("foo", "bar");
}
function bar () {
  return add_number_number(1, 2);
}
function baz () {
  return add_string_string("foo", 123);
}

While this is probably not the kind of optimization you’d want to perform in the browser (because of the increased code size), it may be practical in node.js.

Loop Invariant Code Motion

This is the process of removing invariants from the body of loops and hoisting them outside of the loop itself. In the following example, the compiler knows that the expression let tau = pi * 2 is constant, and therefore can be computed once, rather than for every turn of the loop:

for (let i = 0; i < 10; i++) {
  let tau = pi * 2;
  foo(tau * i);
}

is transformed to:

let tau = pi * 2;
for (let i = 0; i < 10; i++) {
  foo(tau * i);
}

While most compilers like V8 already perform this optimization themselves, doing it ahead of time via a plugin is still worthwhile when combined with other optimizations like…

Loop Unrolling

Not all engines perform this straightforward optimization (V8 doesn’t). Loop unrolling simply means taking a loop with a known bounds, and removing the loop. For example:

for (let i = 0; i < 10; i++) {
  console.log(i);
}

is tranformed into:

console.log(0);
console.log(1);
console.log(2);
console.log(3);
console.log(4);
console.log(5);
console.log(6);
console.log(7);
console.log(8);
console.log(9);

which is slightly faster.

The greater point here is that while the JIT can do some amazing things to achieve phenomenal performance in many cases, there’s still a lot we can do to make it go even faster. Doing all of these optimizations manually would lead to sprawling, unreadable, unmaintainable code, but by delegating this work to our transpiler, we get to write nice, idiomatic JavaScript, and have our toolchain take care of the rest.

We’re planning on releasing a number of plugins that perform this kind of magic, if you’d like to find out more you could contact us or follow us on twitter. For more information about the different kinds of optimizations JavaScript engines can make, take a look at Vyacheslav Egorov’s wonderful blog.

Other Plugin Ideas

Babel plugins are not restricted to improving performance, they enable a lot of cool stuff, for example:

  • i18n / translation transformer which replaces certain strings and expressions with their translated equivalents.
  • custom logging system with arbitrary log levels, set an environment variable to get fine grained logging for certain files / functions.
  • Compile time transformers for alternative templating systems like mustache or handlebars - take a tagged template string and turn it into JavaScript code directly.
  • Documentation generators leveraging Flow type annotations and type inference.

Some of these ideas are already being implemented!

Summary

Babel is an amazing piece of software that is sure to become an addition to virtually every web developer’s toolkit. It is likely to become the main driver of ES6 + ESNext adoption in the near future and helps make writing modern, fast JavaScript a lot more fun and rewarding. Its plugin system differentiates it from competitors such as CoffeeScript and TypeScript and has the potential to drive new innovation in JavaScript. If you’re not already using it, you should definitely check it out.