Improve Your Page Performance With Lazy Loading
Not too long ago, JavaScript made it possible to add nice effects to a page. And with the emergence of libraries, robust applications are emerging built on top of Prototype, Dojo, YUI, and other toolkits. Unfortunately, the people that pay the price for these so-called ‘thin clients’ are the end users. Every JavaScript file that needs to be loaded is a new connection to the server, and every connection to the server means more data; all this occurs before the end user even gets to interact with the page. Thankfully, there is a better way to do this—loading pieces of your web application as the user wants them.
"As an alternative to XHR-style includes of JavaScript files, some libraries such as YUI offer the creation of dynamic
script
nodes for including files. What these tools gain in cross-domain functionality and multiple asynchronous connections, they lose in ease of use. Additionally, since the call doesn’t wait to complete, some form of validation is needed before running our callback."
The important things to address are page weight and load time. Both of these factors have a negative impact on the user, and we should be working towards minimizing it. Gaia Online and Zimbra have both talked recently about their lazy loading experiences. When the switch to lazy loading was made, both companies reduced their page weight by about 200KB and took at least two seconds off their load and initialization time.
Of course, lazy loading isn’t necessarily for everyone. If all of your site pages need all of your JavaScript before the page completes loading, then a lazy load won’t accomplish much. But if you’re working on a complex site or Web 2.0 application, read on!
Lazy Loading, Theory and Terminology
In conventional computer science, the lazy load pattern is a practical implementation of the Proxy Design Pattern. The goal is to avoid loading an object until it is absolutely needed—only then do we invest the resources in loading it. In the case of JavaScript, our application has a common set of function calls used by the web page. Our goal is to implement all of those functions with none of the “real” code. We’ll retrieve the real code later, when we need it. To a developer using our object, though, we want something that will look, act, and behave just like the original object. This specific implementation of the Proxy Design Pattern is called a Ghost.
In the Ghost pattern, we will provide an object that contains all of the public methods of our original object, but they will just be empty stubs with calls to our lazy load operation. When the object is actually used, we’ll load the real object, replacing our shell with the real one. We can then call the same method again, referencing the object we just loaded. Subsequent calls will hit the real object, as the shell no longer exists.
For this article, I’ll be using what is probably the most over-complicated Hello World object in the, um, world:
var HelloWorld = {
sayIt: function() {
alert('Hello, I came from script loaded on the fly!');
}
};
Our application has one public method—sayIt()
—which provides an alert to the screen.
Switching to a Shell Object
To switch to a shell object we are going to need to add a few things to our HelloWorld
object. The new method and property are unique to the lazy load process, and allow other components to know when something has finished loading—they are the is_loaded
property and lazyLoad()
method. We add these to our HelloWorld
application:
var HelloWorld = {
is_loaded: false,
lazyLoad: function(callback) {
// placeholder
},
sayIt: function() {
var args = arguments;
HelloWorld.lazyLoad(function() { HelloWorld.sayIt.apply(HelloWorld, args); });
}
};
In the above example, we’ve also replaced the sayIt()
method with our call to the lazyLoad()
method. Before going further, let’s look at what exactly sayIt()
is doing.
The first line of sayIt()
stores the function’s arguments
in the variable args
, while the second line calls our lazyLoad()
method. The only parameter passed into lazyLoad()
is an anonymous function. The body of the function contains an apply
statement. When run, the method sayIt()
gets called during the callback, using whatever the current HelloWorld
object is, thus preventing us from leaving the old HelloWorld.sayIt()
bound to the callback. This would also need to be done for onclick
events, etc. It ensures that you call sayIt()
on whatever HelloWorld
object is currently loaded.
Retrieval of the Real Object
The next step is to write the lazyLoad()
method in our object. There are two main ways of loading scripts into our page: XMLHttpRequest (XHR) and dynamic script tag nodes in the DOM. If you are using a library, chances are there is already a load utility built in. For our examples, we’ll be showing how lazy loading is done in both the Dojo Toolkit and the YUI Library. For frameworks that don’t include a loading utility, I’ll also provide examples using a standalone class called JIT.
Lazy Loading With XHR and Dojo
The Dojo toolkit provides a directive for including Dojo package files, called dojo.require()
. The loader for Dojo prevents modules from being loaded twice, and ensures code execution stops during the inclusion process, because Dojo uses a synchronous request to grab the JavaScript file and then evaluates it into the window
scope. This is, bar-none, one of the cleanest ways to perform a lazy load, as you are not worrying about callbacks, cleaning up script nodes, or anything of the sort. Of course the XMLHttpRequest won’t work cross-domain, so if your web page is on www.example.com and your scripts live at js.example.com, you will run into problems.
To execute a lazy load in Dojo, we simply make a dojo.require()
call, followed by the callback if it exists. We’re freed from any sort of testing if the module exists thanks to the way the synchronous XHR works.
var HelloWorld = {
is_loaded: false,
lazyLoad: function(callback) {
dojo.require('dojo_ex.helloworld');
if (callback) {
callback();
}
},
sayIt: function() {
var args = arguments;
HelloWorld.lazyLoad(function() { HelloWorld.sayIt.apply(HelloWorld, args); });
}
};
Lazy Loading With Dynamic Script Nodes in YUI and JIT
As an alternative to XHR-style includes of JavaScript files, some libraries such as YUI offer the creation of dynamic script
nodes for including files. What these tools gain in cross-domain functionality and multiple asynchronous connections, they lose in ease of use. Additionally, since the call doesn’t wait to complete, some form of validation is needed before running our callback.
YUI’s Loader Utility has a few things it needs in order to work properly. We need to add the module’s information to the loader; tell the loader we require it; and then assign the callback to the loader’s onSuccess
handler. In YUI, calling loader.insert()
starts the loader, creating script
nodes for all required modules and then calling the onSuccess
callback when those nodes have completed:
var HelloWorld = {
is_loaded: false,
lazyLoad: function(callback) {
var loader = new YAHOO.util.YUILoader();
loader.addModule({
name: "helloworld",
type: "js",
fullpath: "yui_ex/helloworld.js"
});
loader.require("helloworld");
if (callback) {
loader.onSuccess = callback;
}
loader.insert();
},
sayIt: function() {
var args = arguments;
HelloWorld.lazyLoad(function() { HelloWorld.sayIt.apply(HelloWorld, args); });
}
};
Similar to YUI’s Loader, the JIT Loader also uses dynamic script nodes but instead of managing packages, it instead manages loaded script URLs. The only other major difference is the addition of a verifier function, which tests our object to ensure it has finished loading before performing the callback. In our case, we’ll look at the HelloWorld.is_loaded
property (bet you were wondering what that was for!) and wait until it changes to true
, indicating our real object has been lazy loaded:
var HelloWorld = {
isLoaded: false,
lazyLoad: function(callback) {
JIT.loadOnce('jit_ex/helloworld.js',
function() {
return HelloWorld.is_loaded;
},
callback
);
},
sayIt: function() {
var args = arguments;
HelloWorld.lazyLoad(function() { HelloWorld.sayIt.apply(HelloWorld, args); });
}
};
Triggering a Lazy Load
In all of the above examples, we already did the heavy lifting on the event binding, but didn’t really touch on it. Much in the same way we wrote our sayIt()
method to unbind the callback using an anonymous function, we will want to be doing the same for our DOM Events. To illustrate, these are the DOM events for each library tool we’ve talked about so far.
// Dojo
dojo.connect(dojo.byId('clickme'), 'onclick', function() {
HelloWorld.sayIt.apply(HelloWorld, arguments);
});
// YUI
YAHOO.util.Event.addListener("clickme", "click", function() {
HelloWorld.sayIt.apply(HelloWorld, arguments);
});
// JIT
addListener(document.getElementById("clickme"), "click", function() {
HelloWorld.sayIt.apply(HelloWorld, arguments);
});
In each case, instead of just passing in the HelloWorld.sayIt()
reference, we instead pass in an anonymous function, which ensures HelloWorld.SayIt()
is called in the proper scope (whatever HelloWorld
is defined as in that moment). As an added touch, we also pass through the arguments, so that libraries which have event handling can get all the required mouse objects. There is no reason we should be restricting our code to load only when the user clicks, however. With a large application, we can also use proximity-based loading and deferred loading techniques, all without modifying the shell that we already built. All we need to do is call HelloWorld.lazyLoad()
to kick off the loading.
Proximity-based loading
Proximity-based loading is the idea that as soon as you the developer believe the user will use a component, you initiate the loading process.
In the above code, we can attach a mouseover
event to foo_launcher
, since the only reason the user might be moving over that container is to click the foo
button.
Deferred loading
Deferred loading, on the other hand, only puts off loading until all the important stuff has finished loading. Several libraries offer the equivalent of an onDomReady call, which would be a good time to add the additional JavaScript files. While this can provide the benefit of everything being loaded ahead of time, if the user performs an action that requires module C, and you were in the middle of loading module A, there will be a slight delay until module A has been fully loaded.
To help illustrate these different techniques, here is a set of pages that highlights all the different lazy load entry points across all three utilities.
Example in Dojo
Example in YUI
Example in JIT
Caveats and Cautions
It’s hard to create perfect code, and every implementation has its drawbacks. In the case of the Ghost pattern, without JavaScript’s support for interfaces, maintainability becomes a bit trickier, as every public method that is in the real object must be stubbed out in the shell object. For most objects, this probably won’t be a problem, but for more complex objects, using an interface pattern will help lower the maintainability cost.
Because of the way objects are loaded (in every case except same-domain Dojo), there is no way to actually use return values from public methods and you will instead need to rely on callbacks. The following code would be impossible to support via a lazy load:
{
sayIt: function(name) {
return "My name is "+name;
}
}
To help avoid this, lazy loading is best suited for event-driven calls, or moved to an external object manager (via the factory pattern).
The final caveat comes back to the funky fn.apply()
syntax we’ve been using inside our anonymous functions. If you just bind HelloWorld.sayIt()
directly in our examples, you’ll discover that while things work well in Firefox, IE actually holds onto a reference of the shell version of the HelloWorld.sayIt()
object. Even though the real object is loaded, IE will continue to call the HelloWorld
that existed while the binding was made. The apply()
method lets us also pass our arguments around, improving the transparency of the lazyLoad call.
0 Comments:
Post a Comment
Subscribe to Post Comments [Atom]
<< Home