How Browserify Works

Browserify is incredibly popular these days, and rightfully so. In this article we'll look at the basics of how it works.

Browserify uses the term entry file(s) to describe where it will start reading a dependency graph, and its output is referred to as a bundle. At its highest level, a Browserify bundle is simply an IIFE, or Immediately Invoked Function Expression. This is of course a simple mechanism to make code run as soon as it is loaded.

The module map

The first argument passed to the IIFE is a map, where the keys are unique numbers and the values are 2 element arrays. A very simple version of this map might look like this:

{
  1: [function (require, module, exports) {
    module.exports = 'DEP';

  }, {}],
  2: [function (require, module, exports) {
    require('./dep');

    module.exports = 'ENTRY';

  }, {"./dep": 1}]
}

As you can see, each module has been assigned a unique number id. The contents of our entry.js file have been wrapped and given a key of 2, and the contents of dep.js have been wrapped and keyed as 1.

Let's look at the entry.js module. The first element of the array is the source code wrapped in a closure that Browserify generates for you. The purpose of this wrapper is to prevent scope pollution and to ensure your code has access to the variables that would otherwise be provided by the Node environment. We'll take a closer look at this wrapper in the next section.

The array's second element is another map, but this time the map is of your module's dependencies. Since entry.js depends on dep.js, expressed as require('./dep'); in the source code, the dependency map has an entry whose key is ./dep and whose value is 1. Of course, 1 is the key for dep.js in the module map. Since dep.js doesn't depend on any other modules, its dependency map is an empty object {}.

This is a greatly simplified example, but you can see how our dependency tree has been recreated in code. This output is generated by browser-pack, one of the libraries Browserify is built upon.

Generated closure

Now that we've seen how the different files are combined and related to one another, let's look at an individual file and how its contents are augmented. The full source of entry.js is simply

require('./dep');

module.exports = 'ENTRY';

but our bundle represents it as

function (require, module, exports) {
  require('./dep');

  module.exports = 'ENTRY';
}

So Browserify has wrapped our code in a closure that specifies a few important arguments. Node, of course, provides a require method in its environment that serves to synchronously load dependencies. The client side, however, is an entirely different beast. There is no require available natively in browsers, so Browserify implements it for us and gives us access to it by passing it into these closures.

The module and exports arguments serve a purpose that should be obvious if you are familiar with CommonJS syntax (also used in Node). CommonJS modules specify which values they expose to the outside world using the module and/or exports variables. In our entry.js module above, its output is specified as the string “ENTRY” by assigning it to module.exports. If we wanted to expose multiple values we could use exports directly, like exports.foo = “FOO”; exports.bar = “BAR”. Once again though, these things don't exist in the browser by default. Browserify to the rescue.

The cache

The second argument provided to the IIFE is the cache of modules defined in any bundles that were loaded before ours. This is almost always going to be an empty {}, so we're actually going to skip it here. Just know that it's basically another module map, defined elsewhere, that might get passed into your bundle when it starts up.

Entry modules

The third and final argument passed to the bundle IIFE is an array of module ids that will serve as the starting point for building our dependency graph. In our case, remember, entry.js has been given an id of 2. Therefore the third argument is [2]. It is an array because you can specify multiple entry files, which is apparently common for running tests, but not something I have seen a lot of.

Making it all work

Now that we know what is being passed, let's talk about what the F in the IIFE actually does. This function also comes from the browser-pack project. Specifically, the prelude.js file. I'd encourage you to click through and read the file itself (it's pretty well commented), but it's actually even simpler than it looks.

Basically, each entry module id is passed into a function where it is looked for in the cache. If it is found, the module's exports property is returned, fulfilling the dependency. If the id is not found in the cache then our own module map is checked.

When an id is found in our map, the generated closure is called, passing in the require, module, and exports arguments, and the module is added to the cache. The module is also given access to its dependency map so that its own require calls can be fulfilled.

If an id is not found in our module map, Browserify will look in any previously loaded bundles for the id. If it cannot be found there, an error is thrown.

Conclusion

Whew! While the basic mechanism used is quite simple, there is a lot of brilliance and intricacy in making it work as well as it does. Not only that, but we've only covered the most basic of scenarios here. Browserify supports all kinds of advanced features like module aliases, external modules, transforms, and all kinds of other goodies. Those topics, however, will have to be covered another day. Until then, I highly recommend giving Browserify a try on your next project. It is productive, performant, and opens the door to tens of thousands of modules available on npm. What's not to love?

Browserify screencasts

If you enjoyed this article, I have a couple of Introduction to Browserify videos over at egghead.io. The first one is freely available, but the second requires a subscription.