Frontend Dependency Management with Browserify

With frontend development moving as fast as it does at Bitly, things can get pretty messy. We found ourselves with piles of unmanaged script tags and little indication of what was still being used in the app’s current iteration. There had to be a better way! Enter Browserify! A few months ago, we embarked upon […]

With frontend development moving as fast as it does at Bitly, things can get pretty messy. We found ourselves with piles of unmanaged script tags and little indication of what was still being used in the app’s current iteration. There had to be a better way!

Enter Browserify! A few months ago, we embarked upon a project to totally overhaul the “Your Bitlinks” interface, and with that came the opportunity to rethink our practices and introduce frontend dependency management. Now the page looks as good behind the scenes as it does in your browser!

So, what is Browserify?

Browserify is a great tool that lets you write your client-side scripts like it’s node, allowing you to use node’s package management and module systems. Each file should be a module and explicitly require() all its dependencies. Then you simply give Browserify your main entry point, such as an overarching app file, and the name of a destination file, e.g.

browserify app.js > bundle.js

This example takes your network of dependencies starting at app.js and streams all the files from your network of dependencies into bundle.js.

(Side note: Being familiar with the node.js environment and NPM, node’s package manager, is essential for picking up Browserify. In the weeks before we started working with Browserify, I wrote and published a node module as a side project. Going through the process, making sure all the pieces were in place— specifically the package.json— so that I could add it into the NPM registry made figuring out Browserify much easier.)

Why use Browserify?

There was no question in our minds that using a frontend dependency management tool was an important step in our app’s development. We considered the field of options, ultimately settling on Browserify. We didn’t need the async loading functionality of Require.js and appreciated the power of using NPM, leaving Browserify as the natural choice.

Leveraging node to bundle our JavaScript provides many advantages over the traditional multiplicity of strictly ordered script tags in HTML files. We found that the Browserify mindset facilitated coding conventions and frontend practices we strive for at Bitly. Bundling your app’s files together is a good practice because it reduces the amount of HTTP requests your app makes in order to load. Furthermore, the resulting code is all contained in an immediately-invoked function expression, helping to keep the global namespace unpolluted, just how we like it.

We especially noticed that using Browserify helps us reduce the amount of code we’re sending to our users, thanks to the module convention and NPM. Because each module must explicitly require() its dependencies, dead code is less likely to stick around. Granted, unused require() statements can still linger in files, but because these statements are conventionally listed at the beginning of the file, it’s easier to catch unused code. Relying on NPM also helps make sure we don’t forget to include code we need. If you don’t require() a necessary package, you’ll get an error when bundling your components. NPM also takes care of resolving versions; if module foo needs one version of a package and module bar needs another, it’s all figured out for you. Being able to npm install modules right from NPM’s registry is a cinch, much easier than, say, hunting down the jQuery source code. NPM also updates packages as the author pushes updates to the registry, provided you’ve declared the module in your package.json file in a way that green-lights new versions using node’s semver syntax. Furthermore, node comes with built-in modules that too can be require()-ed and therefore bundled. We use the node url module, which make sense because URLs are a little important to our day-to-day. Even more, you don’t have to worry about include order because if a module needs another module, it’ll require() it itself, ensuring the necessary code is included before it is ever used.

One of the most powerful and unique things about Browserify is the ability to use source transforms, packages that alter your source code as it’s streamed to your bundle. For example, we use coffeeify for translating CoffeeScript to JavaScript. Once you’ve installed coffeeify, you can just do

browserify -t coffeeify app.coffee > bundle.js

and your CoffeeScript becomes JavaScript without intermediate build steps.

Transforms depend heavily on node’s streaming interface, but you don’t need to know much of anything about streams to plug in other people’s transforms (they’re fascinating though; read up here if you’d like). I personally liken transforms to Legos— the ones I need generally already exist and it’s just a matter of rummaging through NPM to find them. Some other transforms we use are:

  • hbsify to precompile handlebars templates so we could require them right from the views that use them
  • browserify-swap to substitute one package or file for another based on an environment variable
  • browserify-shim to make “CommonJS-Incompatible Files Browserifyable”
  • watchify to automatically recompile files when a watched file gets edited
  • remapify to “map whole directories as different directories to browserify”

It all gets even better when combined with Browserify’s --debug flag, which adds source maps in a cinch. Source maps are useful because all of the modules get bundled into a single file, making tracking down errors from the browser’s developer tools hard. They become even more important if you decide to use a minification transform such as uglifyify. Instead of getting a reference to the JavaScript bundle, we can see where the bug is in the individual, unbundled, human-readable source file.

Notable Challenges

All in all, we were able to get Browserify up and and running pretty smoothly. Granted, our job was much easier because we weren’t trying to migrate much existing code, and thus did not need to translate it to adhere to the CommonJS standard. Still, transitioning to Browserify was no small feat. There were some interesting roadblocks we ran into because of some of our established development practices.

Something we needed to reconcile early on was the fact that Backbone requires Underscore, but we use LoDash for all its extra gadgets. Terin Stock has an excellent solution involving browserify-swap in this blog post. Long story short, we needed to request that underscore.js itself resolve to LoDash. (Tip: browserify-swap can be a little finicky; make sure you bundle from the directory where node_modules is located.)

We also heavily use Handlebars, and therefore historically depended on Handlebars helpers. However, the fact that they get concatenated together into one huge file doesn’t stick to the modular mentality we’re striving for. Thus, we decided to forgo the Handlebars helpers lifestyle in favor of utility modules called in our views that preprocess the data before it gets fed to Handlebars. Now, if one view needs a function to format large numbers to have commas, all of the other views don’t necesarily know about that function. This will also ensure that once a helper becomes obsolete, it won’t be included, which is a peril with the usual monolithic Handlebars helper registering system.

Another challenge we faced was that our organizational preferences for our code lead to an extremely nested directory structure. Because of this, remapify has been a great asset. Before using the package, relative paths for require() statements for files not in node_modules often needed four or five levels of ../../. This was both hard to parse and hard to figure out right the first time. Remapify allows us to reference files top-down from where we knew they lived, i.e. in our “models” or “views” directories, instead of relative to where they were being required from. We needed to do some forking to fit our exact needs, however.

We’re not that Special!

Well, maybe a little special… but you are too, and you too can use Browserify! If you’re not starting a new project, the most cumbersome part will be likely be making all your modules adhere to Common JS— “node-able” as I like to say. This means using the require() and module.exports syntaxes and making sure everything is require()’d, npm install-ed, and is in a package.json. From there, you’re just a few keystrokes away from getting Browserify running from the command line!

So, if you’re deciding whether or not to use Browserify, even in production, I would highly recommend it. It’s a bundle of fun, will transform your code, and make your app load(s) better!

Source: Bitly