Matt Sweetman

Latest articles

Using the new RequireJS bundles feature

A couple of months ago a new feature was added to RequireJS that allows you load modules from something called 'bundles'. Before going into what bundles are and how they might be useful lets take a look at some of the problems of developing javascript heavy apps.

Optimizing and minifying your scripts down into a single .js file is considered best practice for front-end apps, and it's very useful - it reduces HTTP requests, something of a precious commodity on mobile devices. However if your project grows in size you could find your optimized js file getting larger and larger. I know of a couple of instances where it's been over 0.5MB. That's a lot of minified javascript given the current state of mobile devices. Not only is it a lot to load over a mobile data connection it's also a lot for the browser to parse in one go. I've seen older devices choke and lock-up temporarily when trying to deal with it.

One approach to this problem is to structure the site differently, perhaps split it into several smaller apps and serve each on it's own page, but RequireJS bundles offer us another way.

Using bundles

Bundles allow you to group together your modules into a collection of optimized script files. RequireJS then only loads each bundle when the app needs a module inside it. For example, you might have a bundle for each complex page of the site - taking this website as an example: 'homepage', 'articleslist', 'projects', etc. and a 'shared' bundle for the site wrapper and any utilities used across the others pages.

Bundles can be defined using the requirejs config object:

requirejs.config({
  bundles: {
    'shared': ['shared/main', 'shared/util', 'shared/site', 'text!shared/templates/site.html'],
    'homepage': ['homepage/home', 'text!homepage/templates/home.html'],
    'articleslist': ['articleslist/articles', 'text!articleslist/templates/articles.html']
    // etc...
  }
});

This is a very simplistic setup, but it tells RequireJS which modules are found in which bundle. When the site is running and it tries to load a module it will look in this config to figure out which bundle to load it from. If the bundle isn't already loaded requirejs will load it. This means your site only loads the minimum bundles it needs to show a page while still benefiting from using optimized and minified scripts.

If you've got an app with just one entry page you'll want to load the modules at runtime to get the most out of this. In the following example the module 'site' contains the site-wide menu, and will respond by loading the relevant section when it's clicked:

define(['site'], function(site) {
  // On clicking to navigate to the articleslist load the module at runtime:
  require(['articles'], function(articles) {
    // RequireJS has now loaded the articles module from the articleslist bundle
  });
});

Note: if you're relying on runtime loading of modules you'll want to use require.js, not almond, as almond deliberately can't load modules across the network (to keep the plugin small and simple).

See the RequireJS docs for more info on how to configure bundles.

Creating the bundles

This is the more complex part of the process, and I recommened knowing a little bit about how the optimizer works before continuing.

You'll need to create each bundle using the RequieJS optimizer, ensuring that you only include the modules you need in each. For this process I used the Grunt requirejs optimizer task grunt-contrib-requirejs.

To make your life easier it's worth separating the individual modules into their own folders, one for each bundle (articleslist, homepage, shared):

src/
|-- app/
    |-- articleslist/
    |   |-- articles.js
    |   |-- templates/
    |       |-- articles.html
    |-- homepage/
    |   |-- home.js
    |   |-- templates/
    |       |-- home.html
    |-- shared/
        |-- main.js
        |-- site.js
        |-- util.js
        |-- templates/
            |-- site.html

You then need to create multiple requirejs grunt tasks to optimize each of the bundles into a minified file. The following is a quick example of how to generate one of these bundles, the 'homepage':

requirejs: {
  homepageBundle: {
    options: {
      insertRequire: false,
      baseUrl: src/app/',
      mainConfigFile: 'src/app/homepage/main.js',
      out: outputFolder + 'out/app/homepage.min.js',
      include: [
        'home',
        'text!templates/home.html'
      ],
      exclude: [
        'shared/main',
        'shared/site',
        'shared/util',
        'text!shared/templates/site.html'
      ]
    }
  }
}

You'll notice that you need to explicitly exclude the shared modules for each bundle. This is because your dependency chains will inevitably pull in shared modules across the codebase, and there's no point including them in each bundle. (See the following section for more info on this.) Also note that you obviously don't want to exclude the shared modules from the shared bundle, so leave the exclude option out in this case.

The only problem with this is the duplicated list of excluded files, it isn't very DRY, and if you have large bundles with dozens of files it isn't exactly clean either. So here's a little function that creates a list of files based on a given folder, allowing you to dynamically generate the include and exclude options.

function generateIncludes(baseUrl, glob) {
  // Join together the path and glob so we can use grunt.file.expand to get an array of files
  var fullGlob = path.join(baseUrl, glob);
  var files = grunt.file.expand({filter: 'isFile'}, fullGlob);

  var includes = files.map(function(url) {
    // Strip off the baseUrl, catering for the fact there may or may not be a trailing slash
    url = url.replace(new RegExp(baseUrl + '/?'), '');
    var ext = path.extname(url);

    if (ext === '.html') {
      // If the extension is .html we need to prefix the include with 'text!'
      url = 'text!' + url;
    } else if (ext === '.js') {
      // If the extension is .js then we need to strip the extension off
      url = url.replace(new RegExp('\\' + ext + '$'), '');
    }

    return url;
  });

  return includes;
}

Now you can simplify the grunt task like so, keeping things neat and tidy:

requirejs: {
  homepageBundle: {
    options: {
      insertRequire: false,
      baseUrl: src/app/',
      mainConfigFile: 'src/app/homepage/main.js',
      out: outputFolder + 'out/app/homepage.min.js',
      include: generateIncludes('src/app/homepage', '**/*'),
      exclude: generateIncludes('src/app/shared', '**/*')
    }
  }
}

Being careful with dependencies

The last thing to be aware of is dependency chains. The RequireJS optimizer follows your dependencies and includes every module it finds along the way. If you're not careful it's easy to create dependencies between different bundles. The simple rules are: The 'shared' bundle should only ever depend on other modules within 'shared'. Other bundles should only ever depend on modules within 'shared' and themselves.