External bundles with browserify and gulp

browserify-gulp

Browserify is a nifty little tool that was originally invented to let Node.js modules run in your browser. A nice side effect of this is that you can use browserify to split up your application’s JavaScript into a well organized modules and then smash them together with a proper dependency management. In the past we’ve used Require.js to do that job, but for us it’s too painful and error-prone when creating bundles for production environments. Require.js also doesn’t play very nicely with Rails and it’s quite difficult to get everything working. Esa-Matti Suuronen has written a nice post comparing Require.js and browserify in depth.

Basic Usage

For those who aren’t familiar with browserify here is an example how we’re using it:

js$ = require 'jquery'
QuestionView = require './question_view'
module.exports = ->
  intializeQuestions = ->
    ...
  initializeProfessions = ->
    ...

  questions = intializeQuestions()
  professions = initializeProfessions()
  questionView = new QuestionView(questions, professions)
  return

In the first line we’re requiring jQuery using the CommonJS require syntax. In the second line we’re requiring one of our views. We’re also exporting a function via module.exports for further usage in our application.

Gulp Integration

For Node.js projects we’re using Gulp for our build chain. Since browserify also uses streams it works together with Gulp like charm:

jsgulp.task 'browserify', ->
  browserifyOptions =
  transform: ['coffeeify']

  gulp.src("#{BASES.src}/javascripts/application.coffee", { read: false })
    .pipe(browserify(browserifyOptions))
    .on('error', gutil.log)
    .on('error', gutil.beep)
    .pipe(rename("application.js"))
    .pipe(gulp.dest("#{BASES.build}/javascripts"))
    .pipe(refresh(lrserver))

Browserify takes the application.coffee, processes it by requiring all the dependencies and then it spits out the bundled application.js that can be used in your HTML. A pretty straightforward workflow - but it has a flaw: When your application grows and your dependencies sum up this gulp task may take a while. While doing our latest project the execution time of browserify was up to 12 seconds.

External bundles

External bundles is a mechanism of browserify that lets the user require dependencies that are not directly processed by the actual build step. We’ve used it to create two bundles: The first bundle contains all vendor JavaScript code like jQuery, D3 and plenty of other stuff. The second bundle contains all our app related JavaScript. First you should list all your dependencies in a separate array:

jsEXTERNALS = [
  { require: "lodash", expose: 'underscore' }
  { require: "jquery", expose: 'jquery' }
  { require: "es5-shim" }
  { require: "rsvp", expose: 'rsvp' }
  { require: "../../#{VENDOR_DIR}backbone-1.1.2", expose: 'backbone' }
  { require: "../../#{VENDOR_DIR}d3-3.4.3", expose: 'd3' }
  { require: "../../#{VENDOR_DIR}jquery.nouislider-5.0.0", expose: 'jquery.nouislider' }
  { require: "../../#{VENDOR_DIR}topojson-1.4.9", expose: 'topojson' }
  { require: "../../#{VENDOR_DIR}matchMedia-0.2.0.js", expose: 'matchmedia' }
]

Now you create two gulp tasks ‘browserify:vendor’ and ‘browserify:application’:

jsgulp.task 'browserify:vendor', ->
  gulp.src("#{BASES.src}/scripts/vendor.js", { read: false })
  .pipe(browserify({
      debug: false
  }))
  .on('prebundle', (bundle) ->
    EXTERNALS.forEach (external) ->
      if external.expose?
        bundle.require external.require, expose: external.expose
      else
        bundle.require external.require
  )
  .pipe(rename('vendor.js'))
  .pipe(gulp.dest("#{BASES.build}/scripts"))

and

jsgulp.task 'browserify:application', ->
  browserifyOptions =
    transform: ['coffeeify']

  prebundle = (bundle) ->
    EXTERNALS.forEach (external) ->
      if external.expose?
        bundle.external external.require, expose: external.expose
      else
        bundle.external external.require

  application = gulp.src("#{BASES.src}/scripts/application.coffee", { read: false })
    .pipe(browserify(browserifyOptions))
    .on('prebundle', prebundle)
    .on('error', gutil.log)
    .on('error', gutil.beep)

  vendor = gulp.src("#{BASES.build}/scripts/vendor.js")

  es.concat(vendor, application)
    .pipe(concat('application.js'))
    .pipe(gulp.dest("#{BASES.build}/scripts"))
    .pipe(refresh(lrserver))

The magic happens in the ‘prebundle’ event that browserify provides. In the first gulp task all dependencies are required and in the second task they’re declared as external. In a last step we’re using gulp to tie both bundles together to a single javascript file. If you make changes to your app then only browserify:application is called - which takes just a fraction of the time comparing to the original single task.

Enter watchify

If you’re still not happy with the speed of JavaScript preprocessing you can replace browserify with watchify. Watchify is also a tool from substack but instead of compiling all the resources from the scratch it keeps a cached copy of all the source files and does incremental builds if something changes:

jsrequireExternals = (bundler, externals) ->
  for external in externals
    if external.expose?
      bundler.require external.require, expose: external.expose
    else
      bundler.require external.require

gulp.task 'watchify', ->
  console.log 'watchify'
  entry = "#{BASES.src}/scripts/application.coffee"
  output = 'application.js'
  bundler = watchify entry
  bundler.transform coffeeify
  requireExternals bundler, EXTERNALS

  rebundle = ->
    console.log "rebundle"
    stream = bundler.bundle()
    stream.on 'error', notify.onError({ onError: true })
      .pipe(source(output))
      .pipe(gulp.dest(SCRIPTS_BUILD_DIR))
      .pipe(refresh(lrserver))
    stream

  bundler.on 'update', rebundle
  rebundle()

With the depicted workflow we were able to speed up our development builds by a huge factor. If you want to learn more about browserify and it's internals be sure to check out the browserify handbook. If you like what you’re reading you should follow @9elements on Twitter.

See it in action?

If you want to see everything in action then checkout our 9elements Academy Bootstrap repository on GitHub (deprecated). It’s a quick start for building static websites but keeping the nice toys like sass, haml and browserify around.

Cofounder of 9elements. Software alchemist since he was a kid. Knee deep in tech with building things in React, Rails or Solidity.

Let's talk about