Script Loading Tips

A somewhat 'undocumented' Atlas technique for loading script sooner than window.onload and improving perceived page performance.

The manner in which a page loads script can have a direct bearing on the perceived performance of the page... here are couple of optimization techniques to be mindful of when referencing script files within a page that cover the "how scripts are loaded" and "when scripts are loaded" aspects of the page:

Script tags vs. programmatic script loading
Often times developers simply add <script> tags directly in the page, either in the <head> section or usually somewhere toward the end of the document. Unfortunately this usage pattern results in sub-optimal behavior. The browser must start downloading the script when it encounters a <script> tag, wait for the HTTP request to complete, process the script and run it before it can parse any further. This is because the browser has to assume that script may contain document.write calls to actually create new HTML content that needs to be parsed before the content after the script is parsed. Document.write is a pretty poor practice given the richer DOM capabilities now available in browsers. It can easily be replaced by script that inserts HTML into a known location.

In Atlas we make the assumption that script files will not contain interspersed HTML content to enable the performance optimization. We still need at least one regular script tag to bring in the bootstrapping code itself. Once loaded, the bootstrapping code contains a script loader that will programmatically create script elements and download scripts in the background as the browser continues to parse the HTML content and render it sooner without waiting for scripts. In the current implementation, the list of script references are declared in an xml-script block, but that is just an implementation detail; the general idea is that programmatic loading of scripts results in faster page rendering, and improved perceived performance.

We've also had some thoughts around how to define dependencies across script files. Admittedly, it starts to get a little complex (hence we haven't done it just yet)... but once that information is available, its possible to load more than one script file at a time. This becomes especially useful when larger, high-traffic sites can afford to set up multiple script domains, and are able to break out of the 2 connections per domain browser limits and truly parallelize script loading.

Loading scripts sooner than window.onload
When pages do adopt the programmatic script loading approach rather than individual script tags, they usually kick-start the download process by handling the window.onload event. This is also what Atlas does by default. For most pages this works fine. However, you may have some pages that bring down multiple css files, or more likely multiple images. The window.onload event is raised when all the linked content is downloaded... and can in fact happen much later than when the document itself has been parsed, the DOM tree has been built up and is ready to be scripted. During this interim, the page is in sort of an indeterminate state, which could make it feel slow. Even worse, the user may start interacting with the rendered page immediately, and may find the page does not function as expected!

The solution to this is simply to start loading the scripts sooner. A good location to kick-start script loading is simply when the browser has completed parsing any content on the page. This way the DOM can be safely accessed from the script, and at the same time, the script doesn't need to wait for things like images to download before it gets to start doing its thing. This is a very simple solution that works well across browsers, and it turns out, Atlas supports this for those who'd like to take this extra step.

Here is what happens under the covers: The key part of the bootstrapper in Atlas is the Sys.Runtime class. It registers for the window.onload event among other things and in turn surfaces its own load event. The Sys.Application class registers for this load event on Sys.Runtime, and starts downloading referenced scripts in response to that event. As a result, by default, scripts are loaded in response to window.onload.

However the Sys.Runtime class also exposes an initialize() method (which is in fact called by the window.onload event handler)... this can be used by the page developer as well to programmatically kick-start the process. Essentially this causes the Sys.Runtime class to stop waiting for window.onload and go ahead with raising its load event right then and there. Calling initialize() repeatedly or after window.onload is simply a no-op. It is quite simple to apply this technique... simply add this little bit of script to the end of your page, for example after the <body> tag:

<script type="text/javascript">
  Sys.Runtime.initialize();
</script>
What do you think of this technique?

This is just scratching the surface in terms of the variety of techniques. There are obviously other techniques in the quest to optimize the script loading behavior including compressing scripts, combining multiple scripts etc. etc. You might want to check out my JavaScript Utilities project for some basic tools to help with these. Care to share any tricks that you think could be successfully applied in a general manner?


[ Tags: | | | | ]
Posted on Saturday, 8/26/2006 @ 3:17 PM | #ASP.NET


Comments

20 comments have been posted.

Simone Busoli

Posted on 8/27/2006 @ 3:45 PM
Hi Nikhil, these days client-side scripts are becoming larger and larger, and maybe one day we'll need to discover some new techniques to make pages load faster, but nowadays I'm pretty sure that script compression (in terms of line feeds and white spaces removal) Gzip compression and script combination yields to great results.
As far as I've experimented with a webcontrol I developed which makes heavy usage of javascript, using those three techniques together lets you go from 106kBytes to 9kBytes. More than 90% saving.
Even supposing that one page requires 500kB of scripts, bringing them to be 50kB would make it affordable even to slow connections.
Let's talk about ViewState instead...

Nikhil Kothari

Posted on 8/27/2006 @ 6:46 PM
As far as view state goes, it is a useful tool in overall set of tools offered to the page and control developer by the asp.net toolbox... and like any tool, it should be used appropriately. I'd prefer keeping the comments on topic - specifically optimizing script loading, so heres a detailed writeup on view state by Dave Reed (a new member of the asp.net dev team): http://weblogs.asp.net/infinitiesloop/archive/2006/08/03/Truly-Understanding-Viewstate.aspx.

Simone Busoli

Posted on 8/28/2006 @ 3:10 PM
I read that post some time ago, it's great.
I didn't mean to go off topic, I just mean that the measures we can adopt for scripts are far enough until we find a way to reduce ViewState as much as 90% too.

Dimitri Glazkov

Posted on 8/28/2006 @ 3:11 PM
Nikhil, have you guys looked at Dean's solution:
http://dean.edwards.name/weblog/2006/06/again/

Nikhil Kothari

Posted on 8/28/2006 @ 3:13 PM
Dimitri, I did read that - one of the team members sent a pointer to this... The approach proposed there seems much more complex and also requires special casing each browser whereas the Atlas approach described here seems to be simpler, works across browsers, and requires much lesser script.

Yaniv Golan

Posted on 8/28/2006 @ 3:15 PM
Why not go all the way and define a clear client-side lifecycle model, similar to the Load/PreRender/Render etc on the server side?
Perhaps the following events would make sense:
PreInit - script should not count on anything to be initialized, other then Sys.Runtime.
Init - DOM is fully loaded, but other scripts may or may not be loaded
Load - everything is up and running
etc.

Since the point-of-invocation for a script depends on its position in the page (<HEAD>, onload, etc), this would require that the script association with a phase in the lifecycle should be defined in the server, up to the server side PreRender event, so that the server can render it (or a reference to it) in the appropriate position in the page. You could create a <atlas:clientScript phase="PreInit"> serverside component for this.

(While you're at it, how's about replacing that onPropertyChanged-if-e.get_propertyName() == "inPostBack" dance with a proper before/afterPostBack? or better yet, beforePostBack(eventTarget, eventArgument), and afterPostBack(eventTarget, eventArgument)?)

The order of initialization issue for JS is quite a pain, any formalization you can put in place in this area would be great.

As for dependency between scripts, again, any formalization would be very appreciated. I like the JSX approach, why not extend this with "module information" - a small XML header for each script that details the module namespace and name, and dependency on other script modules.

<script name="myScript" namespace="myLib.myUtils">
<requires>
<scriptRef>myLib.myUtils.myOtherScript</scriptRef>
<scriptRef>myLib.myUtils.myThirdScript</scriptRef>
</requires>
<code>

// hmm... it would be great if myFunc1 would be automatically associated with myLib.myUtils namespace :)
function myFunc1() {
...
}

</code>
</script>

Nikhil Kothari

Posted on 8/28/2006 @ 3:26 PM
Yaniv, the client-side lifecycle model already exists: its basically loading, loaded, running, unloading and unloaded.
The Sys.Application object offers the loaded event, and the unload event. In script#, I've enhanced the Application object to also have the unloading event. The load event is where everything is up and running. An app spends most of its time in the "running" phase, where the user interacts with the app, and the app modifies the page in response.

The ability to load scripts at certain phases is directly related to preloading/delayed-loading type of optimizations. More thought needed in terms of how this may be put into practice.

The property change events for inPostBack are just a hack for now. We will indeed have true events which will open up a number of interesting scenarios.

For script# I've been thinking of building a dependency model using assembly references... but that model could be built manually by the app developer for native script files ... its just painful, and perhaps not something that script developers are used to.

Mark Hildreth

Posted on 8/28/2006 @ 4:31 PM
Nikhil, how does one set up a page to execute script AFTER ALL of the Atlas scripts have been downloaded? The specific case I'm interested in is adding a script reference using the script manager for calling webservices from clientside script. I've tried using the OnScriptLoad property of atlas:ServiceReference, but it seems to have no effect in either the june or july ctp's. Am I missing something?

Nikhil Kothari

Posted on 8/28/2006 @ 6:41 PM
Mark, you have a couple of options... Either you can write a global function named pageLoad like:
function pageLoad() { ... }
Or you can explicitly subscribe to the load event of the Application class:
function foo() { ... }
Sys.Application.load.add(foo);

Actually the load event is raised after scripts have been loaded, and any objects defined declaratively via xml-script have been instantiated.

kentaromiura

Posted on 8/29/2006 @ 7:40 AM
are you sure that this approach is correct?
I think that running code just after the body not assure at 100% that you have the DOM totally loaded, I'm sure that it not correct to assume that DOM is full loaded,
Dean Edwards have already solved this big issue. (http://dean.edwards.name/weblog/2006/06/again)
He also have tested this tecnique in the past: http://dean.edwards.name/weblog/2005/02/order-of-events/ and he said that is not 100% reliable (more like 98%).

Nikhil Kothari

Posted on 8/29/2006 @ 5:06 PM
The DOM is scriptable at that point... even if it may not have been rendered... because the browser is parsing content along the way. The important thing is script can run against it. In fact it must do so, for the same reason that it can't load script in an async manner (the script may introduce more content via document.write). So I think the simpler model works just as well, and you don't need to go down the more complex path you reference. Simplicity is a good thing...

Furthermore, lets look at what code you're likely going to run at that point: you're essentially going to do things like start script downloads, which are by nature async. The DOM will have long been fully loaded and likely rendered before the first line of script even gets executed!

Ryan Heath

Posted on 8/30/2006 @ 1:16 AM
Funny, you should have posted this blog much earlier... :)
http://forums.asp.net/thread/1266307.aspx

I call it at the end of the body, so all contained xmlscript get initialized too.

It really made a difference to the experience of the user! User functions are "immediately" available to the user, instead of having to wait for all content to be downloaded (from various hosts potentially).

OT: is it possible to have script references placed right after, of close after the load of the atlas.js ? Instead of having those references within the xmlscript? So they are downloaded a lot earlier.

// Ryan

kentaromiura

Posted on 8/30/2006 @ 2:36 AM
pay attention at this link:
http://javascript.nwbox.com/formsLoaded/ie-formvalues.php

now add this script after the body:
<script>
window.alert('Append "onload" event.\n\nInline Value: ' + inlineValue + '\nonload Value: ' + getControlValue());
</script>

you can notice the error in IE after the back button.

using this code (in head tag):
<script>
// Dean Edwards/Matthias Miller/John Resig

function init() {
// quit if this function has already been called
if (arguments.callee.done) return;

// flag this function so we don't do the same thing twice
arguments.callee.done = true;

// kill the timer
if (_timer) clearInterval(_timer);

window.alert('Dean "onload" event.\n\nInline Value: ' + inlineValue + '\nonload Value: ' + getControlValue());
};

/* for Mozilla/Opera9 */
if (document.addEventListener) {
document.addEventListener("DOMContentLoaded", init, false);
}

/* for Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");
var script = document.getElementById("__ie_onload");
script.onreadystatechange = function() {
if (this.readyState == "complete") {
init(); // call the onload handler
}
};
/*@end @*/

/* for Safari */
if (/WebKit/i.test(navigator.userAgent)) { // sniff
var _timer = setInterval(function() {
if (/loaded|complete/.test(document.readyState)) {
init(); // call the onload handler
}
}, 10);
}

/* for other browsers */
window.onload = init;

</script>
just work fine.

another thing, put script out of head or body is not allowed by the w3c standard.

Nikhil Kothari

Posted on 8/30/2006 @ 9:02 AM
I am not proposing writing inline code to access form values etc. at the end of the document like you're trying to do with your sample above just to show things that don't work - what I am proposing is starting the script loading process before the document fully loads. Like your sample, which calls the init() function "later", Atlas also calls the pageLoad method "later" - where its safe to do the things you are doing. And that works just fine.

There are always edge scenarios that can break any approach (in fact, its probably the case that any technique in the browser can be broken, given the DOM was never designed for the stress test that Ajax apps put it through). The point is to find one that is simple, and meets the requirements. So yes, if you want to specifically want to look at form values and the like before the document loads, then by all means use the script above. If you want just a simple way to load scripts before the document fully loads, then the simple approach suffices. Personally, I think its more likely that app script will be off in some script library that needs to be loaded, and what you want is a quick way to bootstrap it into existence.

Scott

Posted on 9/1/2006 @ 8:53 AM
While we're on the note of script performance...

I don't know how to describe this bug other than a "memory leak". Nikhil, when I refresh a page using ScriptFX the memory usage of IE goes up and up and up, until finally my computer becomes unusable. It's not just my computer either -- anyone who visits the page.

I read that "IE javascript memory leaks like a sieve" (http://javascript.weblogsinc.com/2005/03/07/javascript-memory-leaks/) but I can't figure out what's causing it or how to even start fixing it

Any suggestions?

Nikhil Kothari

Posted on 9/1/2006 @ 10:59 AM
Scott, please follow up with me offline along with more information about what your app is doing. Things to look into first:
- Check your scenario against the newest build (0.1.5.0) - there was a bug I fixed related to app unloading that could potentially affect this.
- Minimize the repro to something small and isolated - otherwise repro'ing the issue with an entire app is almost a non-starter for these types of issues. Having a small repro will also be useful in my being able to help do some investigation as well.

By the way the bug list for the script# project on http://projects.nikhilk.net is a better channel for this - comments aren't suited for tracking, back and forth discussion, or incremental modification of information.

James

Posted on 9/7/2006 @ 12:09 PM
Does Atlas currently support XML-script blocks (client controls) being defined in a separate file from the .aspx file ?

I have not been able to successfully accomplish this (using something like <script type="text/xml-script" src="components.xml"></script>).

Nikhil Kothari

Posted on 9/12/2006 @ 8:07 PM
No, there isn't a reference/include model for xml-script. XML-script typically defines instances of objects specific to a particular page. Think of it as the behavior part of the page expressed declaratively, just like the UI part of it is expressed in HTML. As such, it typically can't be shared across multiple pages... hence we didn't attempt to add support for including it as a reference.

aharon haravon

Posted on 11/3/2006 @ 12:30 PM
Hello Nik,

In response to your above argument, I have a further question. In client centric scenarios, the whole application is typically in a single aspx page. Still, not all the page content will be loaded, and also not all the page behavior will be loaded upon the initial page markup parsing. So, how does one go about loading xml scripts + associated html at runtime?

Nikhil Kothari

Posted on 11/10/2006 @ 9:14 PM
aharon, typically you will use a ScriptLoader of sorts that can be given a set of URLs to download dynamically, as you app executes, and needs to load functionality on the fly.
The discussion on this post has been closed. Please use my contact form to provide comments.