By Michael Woloszynowicz

By Michael Woloszynowicz

Sunday, April 10, 2011

Dojo Package Loading Hacks and Best Practices

One of key strengths of the Dojo Toolkit is its class system and package loader. It allows you to neatly structure your code and load classes on demand when they are needed. While the package loader is fairly straightforward on the surface, there are a few nuances that are worth exploring. It's also important to understand how best to structure your classes and when to load them so that you optimize not only the load time, but also the users perception of load time. This article assumes you understand the basics of the dojo.provide and dojo.declare functions as its focus is the dojo.require function which does the actual loading. If you're not too familiar with them, I refer you to the this article from DojoCampus before you proceed any further.

A sample class that we will use throughout this article
//Register our packages with the class loader in some other file
dojo.registerModulePath("my", "../../scripts"); 
//Tell the class loader that this class exists and will be declared
dojo.provide("my.loader.Example"); 
dojo.require("my.loader.firstlevel.Dependancy"); //Global level require
dojo.require("my.loader.Base"); //A base class the class will extend from
//Declare the actual class and extend a base class
dojo.declare("my.loader.Example", [my.loader.Base], { 
  someArray: [ ] //Don't ever do this! Treated almost like a static variable

  //The constructor
  constructor: function(args) { 
    this.helper1 = new my.loader.firstlevel.Dependancy();
    this.someArray = [ ]; //This is the right way to initialize a class level object/array
    this.loadRequirements();
  },

  //Load in all the requirements we need throughout our class
  loadRequirements: function() {
    dojo.require("my.loader.secondlevel.MyFirst");
    dojo.require("my.loader.secondlevel.MySecond");
  },


  //A method that does an on demand load
  doSomething: function() {
    dojo.require("my.loader.secondlevel.SecondDependancy");
    var d = new my.loader.secondlevel.SecondDependancy();
    return d.doSomething();
  }
});
As you can see above, we have a simple class that extends a base class, and has required  the Dependancy class that it instantiates in its constructor. To help understand this example let's discuss the mechanics of the require function.

The Require Function
The dojo.require function takes a string argument that is the canonical class name of the desired class. In the case of our Dependancy class, Dojo assumes the class will be located in [some_root]/my/loader/firstlevel/Dependancy.js and if it's not then the require will throw an exception. If you are loading in native Dojo classes that's all that is needed to make a class available for use. If however, you are requiring a class you've written that is located outside the dojo root folder, you must use the dojo.registerModulePath method we've used above. This tells the package loader where to find your package, thus defining the [some_root] value in our path above. It's generally best to call registerModulePath in a template file to ensure that it's added to every page of your web application. Once the JS file has been downloaded, Dojo will make any classes specified by dojo.provide available for instantiation.

The require function is synchronous provided it's being loaded via the normal loader. If however, Dojo was created with an xdomain loader and is housed in a module path that is a whole URL, Dojo will make a cross-domain load that is asynchronous. This obviously has important implications as you can no longer assume that a class will be available immediately after a call to dojo.require, instead you'll have to use dojo.addOnLoad to ensure it has loaded. For the remainder of the article we'll assume you're using the normal loader as this is the most common case.

Finally it should be noted that multiple calls to dojo.require do not hurt performance. Dojo knows when it has loaded a package and will therefore not make redundant requests for the required file.

The Four Levels of Require
There are effectively four levels at which you can require your classes: the global level, the package level, the class level, and the on demand level.

Requiring classes at the global level loads in all class dependencies specified in dojo.require as soon as the page loads, this naturally has implications for the load time of your page. If you make a series of require calls in the header of your page, the page content won't load until all required modules, and their package level modules have loaded. The typical scenario is to require whichever classes are going to be instantiated within the page and then relying on the package level loading to ensure all dependencies are made available to those classes.

Requiring classes at the package level - as seen in our Dependancy class above - causes the class to be downloaded as soon as the JS file/package containing that require statement is loaded via a call to dojo.require. In our example above, if our global scope were to make a call to dojo.require("my.loader.Example") it would first download /my/loader/Example.js and in turn download /my/loader/Base.js and /my/loader/firstlevel/Dependancy.js assuming these are the files the classes are contained in, but more on that later. As mentioned above it should be up to the specific package (or file) to ensure that it makes dojo.require calls for every class that it depends on. Failing to do so and relying on global level requires can cause problems when your class is reused in other contexts.

Requiring classes at the class level can be thought of as loading in required classes once a class is instantiated. In our above example this happens in both the constructor and via the call to loadRequirements. Class level loading is useful when you want to defer file download until the actual class is instantiated, but when you want every dependency to be available throughout the class. For example, if your class yields a wizard style page that uses different Dojo widgets at each step of the wizard, you can choose to load every possible widget at the class level, or you can load just the widgets that are needed for the currently shown wizard page (on demand loading). The former results in quick rendering of each wizard page since the widgets are already downloaded, while the latter results in a faster initial page load, but slower page switching since the unloaded widgets must first be downloaded.

Requiring classes on demand simply causes packages to downloaded when they are needed. We described this scenario above when discussing the multi-step widget example. Once again with on demand loading you are deferring the load time to a later point.

So When Do I Require It?
There is no one size fits all solution as to when packages should be loaded but there are a few tips that apply in a majority of cases.
  • At the global level (or window level), only require the classes/packages that you plan to instantiate at the global level
  • At the package level, only require the classes/packages that your declared classes extend from
  • At the class level, only require all dependencies upon construction if wish to ensure optimum response time during use, otherwise defer loading the package until it is used (on demand)
  • Use on demand loading if you are willing to deal with a slower response time at the time of use. For example a button click may trigger a package download before some action takes place. If this is the case be sure to provide loading feedback to the user so they understand that a delay exists. 
  • Use on demand loading for components that may not execute, thus never need to be loaded
Since the global level and package level loading is clearly defined, the real trade-off exists between class level loading and on-demand loading. When you look at this more carefully it actually becomes a usability issue as it affects the speed, or perceived speed of your application. 

Complex pages can result in an immense amount of packages being downloaded as they may use a large number of widgets, each of which has quite a few dependencies of its own. Pre-loading these widgets at the class level may seem silly since it will delay the initial load of your application. If however, it is guaranteed that the end user will end up loading each widget throughout the life cycle of the page, it's often a good decision as it will make the users experience on that page much smoother. This strategy only works on pages that are used infrequently, as users will be more willing to tolerate an initial wait. If you have a page that users access frequently, it's best to delay loading for as long as possible to improve the initial display of the page. Under both scenarios, if you choose to bulk load a number dependencies, it's wise to provide a progress bar. The movement and feedback of the progress bar gives the end user an impression that the load is quicker than it actually is. Although it's nice to add this, it's not as simple to implement as one might expect. Even adding an animated gif is non-trivial as the single-threaded nature of JavaScript causes the animation to freeze during calls to dojo.require. To help you implement this in a cross-browser manner I've provided a template class below. 

Loading packages with a progress bar
dojo.provide("my.loader.Example"); 
dojo.declare("my.loader.Example", null, { 

  //The constructor
  constructor: function(args) {
    var prog = addInitialUI(args.rootDomNode); 
    this.loadRequirements(prog, function() {
      prog.destroyRecursive();
      dojo.empty(args.rootDomNode);
      this.addActualUI(this.rootDomNode);
    });
  },


  //This builds the rest of the content of your page
  addActualUI: function(/*DOMNode*/ insertIn) { //Do Something },

  //Build our progress bar
  addInitialUI: function(/*DOMNode*/ insertIn) {
    //We need to at least load this now
    dojo.require("dijit.ProgressBar");
    var progressHolder = dojo.create('div', {}, insertIn);
    var progress = new dijit.ProgressBar({ style: { width: "300px" } }, progressHolder);
    return progress;
  },

  //Load in all the requirements we need throughout our class
  loadRequirements: function(progressBar, onFinished) {
    var rqs = [ ];
    rqs.push("my.loader.samples.Package1");
    rqs.push("my.loader.samples.Package2");
    rqs.push("my.loader.samples.Package3");
    rqs.push("my.loader.samples.Package4");
    //etc.
    var load = function(pos) {
      if (pos < rqs.length) {
        progressBar.update({ maximum: items.length, progress: pos });
        dojo.require(rqs[pos]);
        //IE won't render the update to the progress bar without a small delay
        dojo.isIE?setTimeout(function() { load.call(this, pos+1), 10):load.call(this,pos+1);
        /*
          To work with asynchronous loading we could do something like this
          instead of the above line
          dojo.addOnLoad(function() {
            dojo.isIE?setTimeout(function() { load.call(this, pos+1), 10):
                load.call(this,pos+1);
          });
        */
      }
      else onFinished.call(this);
    };
    load.call(this, 0);
  }
});

Anything Else I Should Know?
It's important to remember that the bulk of the load time is spent not on the size of the individual elements (to a point), but rather on the number of elements loaded. As a result, when writing your classes it's wise to group like classes or tightly coupled classes together into one file to reduce load time. A good example of this is Dojo's tree class which groups the Tree and TreeNode classes together into one file, since a TreeNode is never instantiated outside of the Tree. If you decide do this, classes like TreeNode should be preceded with and "_", e.g. _TreeNode, to indicate that they are private and shouldn't be constructed. While it's not ideal from a structure standpoint, grouping like elements together into one file is an option when load time is paramount. For example if you create a Tree class and decide to extend it into a CheckBoxTree class, requiring the CheckBoxTree class will result in two requests, one to Tree.js and another to CheckBoxTree.js. Another option is to include the CheckBoxTree class together with the Tree class in a file called Tree.js. What's important to remember is that when you want to use CheckBoxTree you must call dojo.require("my.Tree"), not dojo.require("my.CheckBoxTree") since Dojo will not find the /my/CheckBoxTree.js file. CheckBoxTree will automatically be made available when you require my.Tree since the provide("my.CheckBoxTree") line will be called upon loading the Tree.js file. Both of these variations are shown below. A better solution to manually grouping similar classes is to use Dojo's build system. Although it involves a bit of setup it's extremely useful for pages that load in a large number of dependencies. More information on it can be found here. If you simply want to group a number of Dojo classes together, Dojo now provides an easy to use, web-based version of the builder that will produce a single minified file in a matter of seconds. Using the builder can drastically reduce the number of individual server requests being made so it's definitely worth exploring.

Grouping Tightly Coupled Classes
//We cannot reverse the order of these declarations
dojo.provide("my._ListNode");
dojo.declare("my._ListNode", null, {
   //Class body
});

dojo.provide("my.List");
dojo.require("my._ListNode"); //Not necessary but good practice
dojo.declare("my.List", [my._ListNode], {
  //Class body
});

Grouping Similar Classes
dojo.provide("my.List");
dojo.declare("my.List", null, {
   //Class body
});

dojo.provide("my.Vector");
dojo.require("my.List"); //Not necessary but good practice
dojo.declare("my.Vector", [my.List], {
  //Class body
});

dojo.provide("my.LinkedList");
dojo.require("my.List"); //Not necessary but good practice
dojo.declare("my.LinkedList", [my.List], {
  //Class body
});

//This function would exist somewhere else
function createAList() {
  dojo.require("my.List");
  var ll = new my.LinkedList();
}

Hopefully this post has given you some more insight on Dojo package loading and the options you have. Remember that your goal is not always minimizing load time, it's also about minimizing the impression of load time.

If you liked this post please follow me on Twitter for more.

No comments:

Post a Comment