Thursday, July 31, 2014

Dynamically loading optimized require.js files

One of the greatest benefits of using require.js is that you're able to structure your code into small, manageable modules, each living in its own file. This is great during development, but loading multiple files as separate HTTP requests is a performance-killer on production.

That is why require.js comes with r.js, an optimizer that combines and minifies related scripts together. If your app is relatively small, the easiest thing to do is to produce a single file with all your JS code combined and minified. However, as your app increases in size, it makes more sense to split your app into bundles, and only loading each bundle if/when it is required.

FooBar Inc - A Sample Application


I wrote a very simple application to demonstrate how to split your app into separate bundles using require.js. You can see the app running on http://examples.alexishevia.com/foobarinc/, and you can grab the source code here.

There are 3 important files on this application:
  • app.js: loads the organization data and delegates content rendering to one of the renderers.
  • renderers/list: renders the company members as a list.
  • renderers/chart: renders the company members as a weird, folder-based organizational chart (it was the fastest thing to code).
The easiest thing to do would be requiring ListRenderer and ChartRenderer from app.js, like this:
/* app.js */

define([
  'organization_data', 'renderers/list', 'renderers/chart'
], function(data, ListRenderer, ChartRenderer){

  function choseRenderer(){
    if(someCondition){
      return ListRenderer;
    }
    else {
      return ChartRenderer;
    }
  }

  function render(el){
    var renderer = choseRenderer();
    el.innerHTML = renderer.render(data);
  }

  return { render: render };
});
If we run the optimizer on app.js, we would get a single file that would contain all of the dependency tree, which is:
  • app.js
    • organization_data.js
    • renderers/list.js
      • underscore.js
    • renderers/chart.js
      • jstree.js
Since it is a very basic application, that wouldn't be so bad. However, imagine we had a lot of renderers, and each of these renderers had many unique dependencies (for example, ChartRenderer loads jstree, which is not used anywhere else on the app).

If we required all of them on app.js, then we would make the client load a bunch of code that could potentially never be used, making our app unnecessarily slower to load.

Loading files dynamically


Let's modify app.js so each renderer gets required if and when it is going to be used, like this:
/* app.js */

define([
  'organization_data'
], function(data){

  function choseRenderer(){
    if(someCondition){
      return 'list';
    }
    else {
      return 'chart';
    }
  }

  function render(el){
    var renderer = choseRenderer();

    // dynamically load renderer
    require(['renderers/' + renderer], function(loadedRenderer){
      el.innerHTML = loadedRenderer.render(data);
    });
  }

  return { render: render };
});
If we run the optimizer on app.js now, only app.js and organization_data.js will be included in our result, because r.js doesn't include dynamically loaded files.
  • app.js
    • organization_data.js
This means we can now run the optimizer on each renderer individually, and each renderer will contain its own dependencies:
  • renderers/list.js
    • underscore.js
  • renderers/chart.js
    • jstree.js
And now our whole application is optimized, but our renderers will only load if they are actually going to be used.

Avoiding duplicate dependencies


Let's say we had multiple renderers that depended on underscore.js. If we optimize each renderer with the default values, then each would get a separate copy of underscore.js, which is not ideal.

Instead, you can optimize the base app.js using "include=underscore", and optimize each renderer using "exclude=underscore". That way underscore will only be included once, on app.js.

To read more about the include and exclude options, see RequireJS Optimizer.

Remember you can take a look at the source code for FooBarInc here. I even included a Gruntfile that shows how to dynamically look for every renderer and optimize it, so you don't have to remember to add a new grunt task every time you add a new renderer.
Post a Comment