How JavaScript bundlers work

First of all, what is a JavaScript bundler? A JavaScript bundler is a tool that puts your code and all its dependencies together in one JavaScript file. There are many of them out there these days, being the most popular ones browserify and webpack.

Why do we need that? Well, the underlying problem is handling dependencies in frontend code. Historically JavaScript hasn’t had a standard for requiring dependencies from your code. There was no `import` or `require` statements. Now we have the new ES2015 import statement, but let’s ignore it for now because it is not widely implemented.

How do you import and export things in JavaScript? How do you make functions of your code visible to the outer world and how do you import functions from other people’s code? The only way has always been through global variables. For example if you want to use jQuery:

<script src="//code.jquery.com/jquery-1.12.0.min.js"></script>
<script>
// `$` global variable available here
</script>

And what if you want to split your own code in comprehensive small files? You will end up with something like this:

<script src="//code.jquery.com/jquery-1.12.0.min.js"></script>
<script src="/js/foo.js"></script>
<script src="/js/bar.js"></script>
<script src="/js/foobar.js"></script>
<script>
// Here goes some code
</script>

Now in the script tag you can use all those dependencies. But what if foo.js depends on bar.js? You must change the order of the scripts. Hmm this is becoming a mess:

  • You are using global variables, which is something we should avoid as much as possible.
  • You need to be careful with the order in which you put the script tags.
  • This will become harder and harder to maintain with more complex dependencies.

How is this problem solved in other environments? Node.js implements its own modules system implementing a require() function and an `exports` object among other things based on this Commonjs modules spec. That’s why you will see this mechanism often referred as “commonjs”.

What if we could do this in frontend code?

<script>
var $ = require('jquery')
var foo = require('./js/foo')
var bar = require('./js/bar')
var foobar = require('./js/foobar')
</script>

That would be great, huh? Well, there’s a technical limitation: require() is synchronous so if we need to require a file that hasn’t been loaded in some way we need to do an HTTP request, but that is asynchronous. So the solution is putting all the dependencies in one file to have all the code in memory, ready to be used when invoking the require() function that the JavaScript bundler will implement some way.

Note: Putting all the code in the same file also prevents doing many HTTP requests which is a performance problem (not in HTTP/2, but it is not widely adopted yet).

Note: CommonJS is not the only module system variant available in some tools. For example webpack implements AMD as well, and AMD can be asynchronous, but let’s focus on require()/exports here.

So now we know what JavaScript bundlers do and why they do it. But how they do it? How the final bundled file looks like and how it implements this dependency handling? Let’s see. This could be the output of a very basic JavaScript bundler tool:

// common code for implementing require()/exports
var dependencies = {} // loaded modules
var modules = {} // code of your dependencies
// require function
var require = function (module) {
if (!dependencies[module]) {
// module not loaded, let’s load it
var exports = {}
modules[module](exports)
// now in `exports` we have the things made “public”
dependencies[module] = exports
}
return dependencies[module]
}
// dependendencies
modules['jquery'] = function (exports) {
// code of jquery
}
modules['foo'] = function (exports) {
// code of bar.js
exports.helloWorld = function () {
console.log('hello world')
}
}
modules['bar'] = function (exports) {
// code of bar.js
}
// etc…
// here goes the code of your "entry file".
// Which is the entry point of your code
// For example:
var $ = require('jquery')
var foo = require('foo')
var bar = require('bar')
foo.helloWorld()

And that’s it. There are a bunch of implementation details not covered here but that’s the main idea.

The usage of a tool like this also opens the door for other interesting things:

  • We can use the npm because require()/exports are implemented the same way than in Node.js so they look up in the “node_modules” directory for your dependencies. This way you have a strenght of npm’s versioning features and you can easily install and publish new libraries.
  • You can write cross-platform code since require() behaves the same as in Node.js. So as long as you don’t depend on specific functionality from the browser or the Node.js core modules. If that’s the case you can also detect easily in which platform your code is running and do different things in each case. For example you can use AJAX if typeof window !== ‘undefined’ and use the http core module if you are in Node.js.
  • These tools optionally allow you to use different transforms during the bundling process. So you can transpile your code from ES6 to ES5, etc.
  • Also some bundlers remove unused code so the final file contains only the code your application needs to run.

Note: webpack goes one step further and adds new functionality to require(). You can require not only JavaScript code but assets such as CSS or images and it can execute transforms/loaders on them. For example:

require("!style!css!less!bootstrap/less/bootstrap.less");

Start using a JavaScript bundler today. Check out browserify or webpack.

Happy coding!

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.