Creating A Lazy Loading Utility Module For RequireJS
Yesterday, I started to look at delaying the loading of RequireJS modules until they were first requested. While I liked this approach for modules that may rarely (or never) be used within an application, I wasn't happy with what the code looked like; the lazy-loading functionality, initialization functionality, and the invocation functionality were all mixed together. This morning, I wanted to take that code and see if could extract the lazy loading logic into its own RequireJS module.
If you recall my blog entry from yesterday, I said that a lazy-loaded module existed in three distinct states:
- Unloaded.
- Loading.
- Loaded.
Of these three states, only the last one - Loaded - really has any bearing on the module being used. As such, I wanted to see if I could encapsulate the states and the state transitions and simply expose the loaded state through callbacks.
In some sense, this is what the require() method already does - it loads your modules and then invokes your callback when the modules have become available. In a lazy-loading scenario, however, we are faced with the problem that the require() function may be run many times, especially if the code is dependent on a user interaction.
This requires us to think a bit more deeply about our calling code. If you look back on the code yesterday, you can see that my calling code has two primary gestures:
- Initializing the lazy-loaded modules.
- Using the lazy-loaded modules.
The bulk of the code, in yesterday's post, deals with managing these two gestures in coordination with the [potentially] asynchronous nature of the require() function. What I'd like to do is completely encapsulate the require() function and then only expose hooks for the two primary gestures of lazy loading:
- Initialization
- Invocation
To do this, I created a lazyRequire.js utility module that exposes a single function, once(). When you call the once() function, it returns a new, unique function, requireOnce(). Like require(), the requireOnce() function takes a list of dependencies; however, unlike require(), the requireOnce() method takes two callbacks: one for module initialization and one for module invocation.
Refactoring the RequireJS bootstrap from yesterday, here is how this lazy-load module can be used to load our FAQ (Frequently Asked Questions) module in a just-in-time fashion:
Main.js - Our RequireJS Bootstrap File
// Set up the paths for the application.
requirejs.config({
paths: {
"domReady": "lib/require/domReady",
"jquery": "lib/jquery/jquery-1.7.2.min",
"templates": "templates",
"text": "lib/require/text",
"utils": "utils",
"views": "views"
}
});
// Run the scripts when the DOM-READY event has fired.
require(
[
"jquery",
"utils/lazyRequire",
"domReady!"
],
function( $, lazyRequire ){
// Since the Help / FAQ module is probably going to be rarely
// used by the user, I don't want to bother loading it as
// part of the initial page load. As such, I'll lazy-load it
// when the "launch" link is clicked.
(function(){
// Our FAQ module will start out as null until loaded.
// And, it's not loaded until it's first needed.
var faq = null;
var body = $( "body" );
var launchFaq = $( "p.m-help a" );
var requireOnce = lazyRequire.once();
// Bind the click handler. This handler will lazy-load
// the FAQ module when it is first requested.
launchFaq.click(
function( event ){
event.preventDefault();
// Run require once, the first time; then, use
// the "run" callback for each subsequent require
// invocation.
requireOnce(
[
"views/faq"
],
function( FAQ ){
faq = new FAQ();
},
function(){
faq.open( body );
}
);
}
);
})();
}
);
As you can see, the requireOnce() takes two callbacks. The first callback is only ever invoked once, when RequireJS has loaded the given dependencies. This callback is used to instantiate modules and initialize the calling code. Once the modules have been lazy-loaded, the second callback is invoked for every subsequent request to the requireOnce() function.
Two important points to note:
- The second callback is implicitly invoked after the first callback has been run.
- No callbacks are runs if the requireOnce() method is invoked during the "Loading" phase of the modules.
Ok, let's take a look at this lazy-loading utility module:
lazyRequire.js - Our Lazy Loading Utility
// Define the module.
define(
[
"require"
],
function( require ){
// Define the states of loading for a given set of modules
// within a require() statement.
var states = {
unloaded: "UNLOADED",
loading: "LOADING",
loaded: "LOADED"
};
// Define the top-level module container. Mostly, we're making
// the top-level container a non-Function so that users won't
// try to invoke this without calling the once() method below.
var lazyRequire = {};
// I will return a new, unique instance of the requrieOnce()
// method. Each instance will only call the require() method
// once internally.
lazyRequire.once = function(){
// The modules start in an unloaded state before
// requireOnce() is invoked by the calling code.
var state = states.unloaded;
var requireOnce = function( dependencies, loadCallback, runCallback ){
// Use the module state to determine which method to
// invoke (or just to ignore the invocation).
if (state === states.loaded){
// Invoke the run callback - the modules have
// been loaded.
runCallback();
// The modules have not yet been requested - let's
// lazy load them.
} else if (state === states.unloaded){
// We're about to load the modules asynchronously;
// flag the interim state.
state = states.loading;
// Load the modules.
require(
dependencies,
function(){
// Invoke the load callback with the
// loaded module definitions so that the
// calling code can use the module
// defitions to lazily initialize code.
loadCallback.apply( null, arguments );
// Update the state - the modules have
// been loaded and the calling code has
// been initialized.
state = states.loaded;
// Explicitly invoke the run callback
// since we always want to use the modules
// after they have first been loaded.
runCallback();
}
);
// RequireJS is currently loading the modules
// asynchronously, but they have not finished
// loading yet.
} else {
// Simply ignore this call.
return;
}
};
// Return the new lazy loader.
return( requireOnce );
};
// -------------------------------------------------- //
// -------------------------------------------------- //
// Return the module definition.
return( lazyRequire );
}
);
As you can see, this utility module encapsulates all of the state management that used to be in the calling code of the previous blog post. And, now that the coordination of the module loading is hidden, the calling code only has to worry about initialization and invocation of the modules.
Want to use code from this post? Check out the license.
Reader Comments
Perhaps I am missing something subtle here, but doesn't RequireJS handle this state internally? Admittedly, I use curl.js instead of RequireJS so I wouldn't know, but it seems unlikely that a second call to require() would re-fetch / re-initialize the previously loaded module. I know for a fact that curl.js does this.
But again, maybe I am missing something subtle in what you are trying to achieve.
@Jens,
RequireJS will only load the Module once, that is correct. My issue is that require() call is inside a click-handler for my user-interface (UI). As such, it will be called every time the user clicks the Help link.
Now, RequireJS will only load the module once - for subsequent require() invocations, it will simply use the module definition that has been cached in memory. That's not the problem. The problem is the use of the modules (in MY code) after they have been loaded by RequireJS.
Once the FAQ module has been loaded, my code initializes it (ie. instantiates it) and caches it locally:
faq = new FAQ();
Since I am using the same instance of FAQ across click-handlers, I don't want to be instantiating it more than once. As such, I need a way to instantiate it on the "first" click handler and then simple "use" it on all the subsequent click handler events.
IF the require() method call was *outside* of the click-handler binding, none of this would matter. However, if were outside the click-handler binding, it would load whenever the page ran - which is exactly what I'm trying not to do for modules that may never actually be invoked.
Does that make more sense?
That's a good point, @Jens. The whole thing could probably be reduced to this.
Or more generally:
@Patrick,
Yes, that's pretty much what it's doing. I was just trying to encapsulate the "initialization management" so that the calling code doesn't have to deal with it.
That said, looking at your second example, I am open to believe that it looks pretty straightforward, if not MORE straightforward than my approach.
Good thinking!
@Ben,
Sure, that makes sense. In my own framework, I use a slightly different approach; I extend* both
and
but keeping the method signatures intact. Then, by defining a magic
function in any module, I get an initializer function which is invoked automatically the first time a module is required. It's not a perfect solution, but it feels a little more OOP to me.
*) Under the hood, I inject an
boolean and a
function handling the logic
@Jens,
Ah, I see - that's definitely an interesting idea.
@All,
Taking another look at this lazy-loading thing, this time from a compiler perspective:
www.bennadel.com/blog/2404-Compiling-Optimizing-A-Subset-Of-A-RequireJS-Application.htm
Using the r.js compiler to build to modules, one of which is lazy-loaded.
I am not convince with this approach. RequireJs always return the previous loaded module instead of calling it again. I am not sure if I am right or wrong. It looks subtle to me also. Take the following example:-
define(function(){
var Module = {
load : function(){
Module.loaded = true
}
}
return Module;
});
//"Module" is defined in paths
require(["Module"], function(Module){
console.log(Module.loaded);//first time it is undefined. But the continuous invoking will return true
Module.load();
});
I am not convince with this approach. RequireJs always return the previous loaded module instead of calling it again. I am not sure if I am right or wrong. It looks subtle to me also. Take the following example:-
This shows that requirejs code is not invoking everytime, it is retuning the cache object.
Although I'm working on a quite different approach in my app I have to say that yor both articles are a blessing.
I'm not trying to be smarter than anyone, but I would rather go with loosely coupled solution. I let my app to handle lazy situations. I only configure module and I start it later ON EVENT.
In my app the other module has the link that only fires the event when clicked. The rest is up to app :-)
http://github.com/op1ekun/SAJA have keep looking at feature/11 branch. Thia is where magic happens B-)
Thanks a million for ideas!