9 min read

The JavaScript code for a page falls into two groups—code required to render the page, and code required to handle user interface events, such as button clicks. The code to render the page is used to make the page look better , and to attach event handlers to for example, buttons.

Although the rendering code needs to be loaded and executed in conjunction with the page itself, the user interface code can be loaded later, in response to a user interface event, such as a button click. That reduces the amount of code to be loaded, and therefore the time rendering of the page is blocked. It also reduces your bandwidth costs, because the user interface code is loaded only when it’s actually needed.

On the other hand, it does require separating the user interface code from the rendering code. You then need to invoke code that potentially hasn’t loaded yet, tell the visitor that the code is loading, and finally invoke the code after it has loaded.

Let’s see how to make this all happen.

Separating user interface code from render code

Depending on how your JavaScript code is structured, this could be your biggest challenge in implementing on-demand loading. Make sure the time you’re likely to spend on this and the subsequent testing and debugging is worth the performance improvement you’re likely to gain.

A very handy tool that identifies which code is used while loading the page is Page Speed, an add-on for Firefox. Besides identifying code that doesn’t need to be loaded upfront, it reports many speed-related issues on your web page.

Information on Page Speed is available at
http://code.google.com/speed/page-speed/.

OnDemandLoader library

Assuming your user interface code is separated from your render code, it is time to look at implementing actual on-demand loading. To keep it simple, we’ll use OnDemandLoader, a simple low-footprint object. You’ll find it in the downloaded code bundle in the folder OnDemandLoad in the file OnDemandLoader.js.

OnDemandLoader has the following features:

  • It allows you to specify the script, in which it is defined, for each event-handler function.
  • It allows you to specify that a particular script depends on some other script; for example Button1Code.js depends on library code in UILibrary1.js. A script file can depend on multiple other script files, and those script files can in turn be dependent on yet other script files.
  • It exposes function runf, which takes the name of a function, arguments to call it with, and the this pointer to use while it’s being executed. If the function is already defined, runf calls it right away. Otherwise, it loads all the necessary script files and then calls the function.
  • It exposes the function loadScript, which loads a given script file and all the script files it depends on. Function runf uses this function to load script files.
  • While script files are being loaded in response to a user interface event, a “Loading…” box appears on top of the affected control. That way, the visitor knows that the page is working to execute their action.
  • If a script file has already been loaded or if it is already loading, it won’t be loaded again.
  • If the visitor does the same action repeatedly while the associated code is loading, such as clicking the same button, that event is handled only once.
  • If the visitor clicks a second button or takes some other action while the code for the first button is still loading, both events are handled.

A drawback of OnDemandLoader is that it always loads all the required scripts in parallel. If one script automatically executes a function that is defined in another script , there will be a JavaScript error if the other script hasn’t loaded yet. However, if your library script files only define functions and other objects, OnDemandLoader will work well.

Initializing OnDemandLoader

OnDemandLoading.aspx in folder OnDemandLoad in the downloaded code bundle is a worked-out example of a page using on-demand loading. It delays the loading of JavaScript files by five seconds, to simulate slowly loading files. Only OnDemandLoader.js loads at normal speed.

If you open OnDemandLoading.aspx, you’ll find that it defines two arrays—the script map array and the script dependencies array. These are needed to construct the loader object that will take care of the on-demand loading.

The script map array shows the script file, in which it is defined, for each function:

var scriptMap = [
  { fname: 'btn1a_click', src: 'js/Button1Code.js' },
  { fname: 'btn1b_click', src: 'js/Button1Code.js' },
  { fname: 'btn2_click', src: 'js/Button2Code.js' }
];

Here, functions btn1a_click and btn1b_click live in script file js/Button1Code. js, while function btn2_click lives in script file js/Button2Code.js.

The second array defines which other script files it needs to run for each script file:

var scriptDependencies = [
  {
    src: '/js/Button1Code.js',
    testSymbol: 'btn1a_click',
    dependentOn: ['/js/UILibrary1.js', '/js/UILibrary2.js']
  },
  {
    src: '/js/Button2Code.js',
    testSymbol: 'btn2_click',
    dependentOn: ['/js/UILibrary2.js']
  },
  {
    src: '/js/UILibrary2.js',
    testSymbol: 'uifunction2',
    dependentOn: []
  },
  {
    src: '/js/UILibrary1.js',
    testSymbol: 'uifunction1',
    dependentOn: ['/js/UILibrary2.js']
  }
];

This says that Button1Code.js depends on UILibrary1.js and UILibrary2.js. Further, Button2Code.js depends on UILibrary2.js. Further, UILibrary1.js relies on UILibrary2.js, and UILibrary2.js doesn’t require any other script files.

The testSymbol field holds the name of a function defined in the script. Any function will do, as long as it is defined in the script. This way, the on-demand loader can determine whether a script has been loaded by testing whether that name has been defined.

With these two pieces of information, we can construct the loader object:

<script type="text/javascript" src="js/OnDemandLoader.js">
</script>
var loader = new OnDemandLoader(scriptMap, scriptDependencies);

Now that the loader object has been created, let’s see how to invoke user interface handler functions before their code has been loaded.

Invoking not-yet-loaded functions

The point of on-demand loading is that the visitor is allowed to take an action for which the code hasn’t been loaded yet. How do you invoke a function that hasn’t been defined yet? Here, you’ll see two approaches:

  • Call a loader function and pass it the name of the function to load and execute
  • Create a stub function with the same name as the function you want to execute, and have the stub load and execute the actual function

Let’s focus on the first approach first.

The OnDemandLoader object exposes a loader function runf that takes the name of a function to call, the arguments to call it with, and the current this pointer:

function runf(fname, thisObj) {
  // implementation
}

Wait a minute! This signature shows a function name parameter and the this pointer, but what about the arguments to call the function with? One of the amazing features of JavaScript is that can you pass as few or as many parameters as you want to a function, irrespective of the signature. Within each function, you can access all the parameters via the built-in arguments array. The signature is simply a convenience that allows you to name some of the arguments.

This means that you can call runf as shown:

loader.runf('myfunction', this, 'argument1', 'argument2');

If for example, your original HTML has a button as shown:

<input id="btn1a" type="button" value="Button 1a"
   onclick="btn1a_click(this.value, 'more info')" />

To have btn1a_click loaded on demand, rewrite this to the following (file OnDemandLoading.aspx):

<input id="btn1a" type="button" value="Button 1a"
   onclick="loader.runf('btn1a_click', this, this.value,
   'more info')" />

If, in the original HTML, the click handler function was assigned to a button programmatically as shown:

<input id="btn1b" type="button" value="Button 1b" />
<script type="text/javascript">
  window.onload = function() {
    document.getElementById('btn1b').onclick = btn1b_click;
  }
</script>

Then, use an anonymous function that calls loader.runf with the function to execute:

<input id="btn1b" type="button" value="Button 1b" />
<script type="text/javascript">
  window.onload = function() {
    document.getElementById('btn1b').onclick = function() {
      loader.runf('btn1b_click', this);
}
}
</script>

This is where you can use the second approach—the stub function. Instead of changing the HTML of your controls, you can load a stub function upfront before the page renders (file OnDemandLoading.aspx):

function btn1b_click() {
  loader.runf('btn1b_click', this);
}

When the visitor clicks the button, the stub function is executed. It then calls loader.runf to load and execute its namesake that does the actual work, overwriting the stub function in the process.

This leaves behind one problem. The on-demand loader checks whether a function with the given name is already defined before initiating a script load. And a function with that same name already exists—the stub function itself.

 

The solution is based on the fact that functions in JavaScript are objects. And all JavaScript objects can have properties. You can tell the on-demand loader that a function is a stub by attaching the property “stub”:

btn1b_click.stub = true;

To see all this functionality in action, run the OnDemandLoading.aspx page in folder OnDemandLoad in the downloaded code bundle. Click on one of the buttons on the page, and you’ll see how the required code is loaded on demand. It’s best to do this in Firefox with Firebug installed, so that you can see the script files getting loaded in a Waterfall chart.

Preloading

Now that you have on-demand loading working, there is one more issue to consider: trading off bandwidth against visitor wait time.

Currently, when a visitor clicks a button and the code required to process the click hadn’t been loaded, loading starts in response to the click. This can be a problem if loading the code takes too much time.

An alternative is to initiate loading the user interface code after the page has been loaded, instead of when a user interface event happens. That way, the code may have already loaded by the time the visitor clicks the button; or at least it will already be partly loaded, so that the code finishes loading sooner. On the other hand, this means expending bandwidth on loading code that may never be used by the visitor.

You can implement preloading with the loadScript function exposed by the OnDemandLoader object. As you saw earlier, this function loads a JavaScript file plus any files it depends on, without blocking rendering. Simply add calls to loadScript in the onload handler of the page, as shown (page PreLoad.aspx in folder OnDemandLoad in the downloaded code bundle):

<script type="text/javascript">
 window.onload = function() {
  document.getElementById('btn1b').onclick = btn1b_click;

loader.loadScript(‘js/Button1Code.js’);
loader.loadScript(‘js/Button2Code.js’);
}
</script>

You could preload all your user interface code, or just the code you think is likely to be needed.

Now that you’ve looked at the load on demand approach, it’s time to consider the last approach—loading your code without blocking page rendering and without getting into stub functions or other complications inherent in on-demand loading.

LEAVE A REPLY

Please enter your comment!
Please enter your name here