A sample class that we will use throughout this article
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.
Loading packages with a progress bar
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
Grouping Similar Classes
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.