2009 Dec 1 @ 12:04am jquery xmlhttprequest even faster web sites

Managed XHR Injection

Update (2009 Dec 2 @ 12:09PM): I started to question if Managed XHR Injection was all unnecessary fluff or if I could just write script tags to the page. Then I reread parts of Chapter 4 and realized that Managed XHR Injection parallelizes downloads and ensures execution order in all browsers, something that neither the Script DOM Element nor document.write Script Tag approach offers. (Script DOM Element parallelizes downloads in all browsers but only ensures execution order in Firefox and Opera. document.write Script Tag ensures order in all browsers but does not parallelize downloads in Firefox.)

Steve Souders recently released a follow-up to High-Performance Web Sites called Even Faster Web Sites.

In Chapter 3, Splitting the Intial Payload, he describes the virtues of splitting your JavaScript into that which must be downloaded as the page is loaded and that which can be downloaded later. The reason being that typical browser behavior dictates that when a script tag is encountered that references an external script, no additional downloads (for scripts, images, or anything else) are allowed until the external script has been downloaded, parsed, and executed. Which means that all explicit script tags in your HTML referencing external scripts constitute a blocking operation.

The drastic example he gives is of Facebook’s homepage, which loads 14 scripts totaling 786 kilobytes. The catch is that only 9% of the functions declared in those 14 scripts are actually called by the page before the onload event is raised, meaning that 91% of the functions are not necessary for the loading and rendering of the page and therefore don’t need to be downloaded in a blocking manner.

In Chapter 4, Loading Scripts Without Blocking, he describes several methods for downloading external scripts after onload in a non-blocking manner. It is important that referenced scripts are executed in the order they are referenced, but the order in which they are downloaded is unimportant. This allows the developer to download scripts in parallel. One method described is XHR Injection, in which a script is downloaded via AJAX, then appended to a new script tag, which is then appended to the document to execute it. The code to do so is trivial, but order of execution is not guaranteed. He goes on to describe Managed XHR Injection, in which downloads are queued in parallel but execution is done in sequence once all scripts are downloaded. He does not provide any code for such a solution so I decided to give it a try:

$.downloadScripts = function(urls)
{
    // Map the URLs to objects containing the URLs and default flags.
    var scripts = $.map
    (
        urls,
        function(url)
        {
            return { url: url, downloadAttempted: false, injectable: false, data: undefined };
        }
    );

    $.each
    (
        scripts,
        function(n, script)
        {
            // Attempt to download script.
            $.ajax
            (
                {
                    url: script.url,
                    // Need to know when errors occur so that these scripts aren't waited on for execution to start.
                    error: function()
                    {
                        script.downloadAttempted = true;
                    },
                    success: function(data, status)
                    {
                        // Save the contents for later and mark it as downloaded.
                        script.data = data;
                        script.injectable = script.downloadAttempted = true;

                        var allDownloadsAttempted = true;

                        for (var n = 0; n < scripts.length && allDownloadsAttempted; n++)
                            allDownloadsAttempted &= scripts[n].downloadAttempted;

                        // If no more scripts are being waited on...
                        if (allDownloadsAttempted)
                        {
                            // Loop over scripts, in original order, and append a script tag
                            // to the body for those that are injectable.
                            for (var n = 0; n < scripts.length; n++)
                            {
                                if (scripts[n].injectable)
                                    $("body").append("<script>" + scripts[n].data + "</script>");
                            }

                            delete scripts;
                        }
                    }
                }
            );
        }
    );
};

It is implemented as a jQuery plugin, which means you’ll need three explicit script tags in your HTML for:

  1. Referencing the jQuery library.
  2. Referencing the file containing downloadScripts.
  3. A call to downloadScripts to download scripts after onload.

So:

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
<script type="text/javascript" src="ManagedXhrInjection.min.js"></script>
<script type="text/javascript">
    $(document).ready
    (
        function()
        {
            $.downloadScripts(["1.js", "2.js", "3.js"]);
        }
    );
</script>

It works by mapping the passed array of script URLs to a new array of objects containing each URL and flags to control execution. It handles the case of a bad script URL by specifying that the download was attempted. When a script is successfully downloaded, it checks to see if there are any more scripts to attempt to download and if not, it creates and appends new script elements for those scripts that were successfully downloaded.

A 362 byte obfuscated and minified version:

$.downloadScripts=function(a){var b=$.map(a,function(c){return{d:c,e:false,f:false,g:undefined}});$.each(b,function(h,i){$.ajax({url:i.d,error:function(){i.e=true;},success:function(j){i.g=j;i.f=i.e=true;var k=true;for(var l=0;l<b.length&&k;l++)k&=b[l].e;if(k){for(var m=0;m<b.length;m++)if(b[m].f)$("body").append("<script>"+b[m].g+"</script>");delete b}}})})};
2009 Nov 20 @ 2:39pm underscore.js jquery

Underscore.js

I’ve been playing with Underscore.js a bit lately. “It’s the tie to go along with jQuery’s tux.” It is not a jQuery plugin nor does it require jQuery in any way. But it is designed to complement jQuery.

About half of Underscore’s 50-ish functions are for working with collections and arrays. Another subset work with objects (comparisons, evaluations, etc.).

However, I’ve been looking at one of Underscore’s utility functions, template. It’s useful for formatting output of asynchronous calls that return JSON.

Suppose you have a User data structure and each User has a Title, a Name, and an EmailAddress. You might setup a server-side script that returns JSON that looks like:

[
    {
	"Title": "Mr.",
        "Name": "John Smith",
	"EmailAddress": "john@smith.com"
    },
    {
	"Title": "Mrs.",
        "Name": "Jane Smith",
	"EmailAddress": "jane@smith.com"
    }
]

Using jQuery, you could add that to your page like so:

$.getJSON
(
    "Users.json",
    function(users)
    {
        $.each
        (
            users,
            function()
            {
                appendUser(this);
            }
        );
    }
);

function appendUser(user)
{
    tr = $("<tr/>").appendTo("tbody#Users");

    $("<td/>").appendTo(tr).text(user.Title);
    $("<td/>").appendTo(tr).text(user.Name);
    $("<td/>").appendTo(tr).text(user.EmailAddress);
}

Or you could rewrite appendUser to use Underscore’s template function like so:

function appendUser(user)
{
    $("tbody#Users").append(_.template("<tr><td><%= Title %></td><td><%= Name %></td><td><%= EmailAddress %></td></tr>", user));
}

This idea isn’t new. You can trace it back to John Resig’s micro-templating (and earlier), but Underscore packages it up nicely into a single function call that isn’t bound explicitly to jQuery as a plugin.

2009 Nov 19 @ 10:14am asp.net-mvc javascript jquery

Even Smaller

This is a follow-up to Replace MicrosoftMvcAjax.js.

I combined the handle and click functions into a single function declaration. That allowed me to reduce two functions into one and remove two convenience functions. I also modified the error and complete callbacks to return the status code instead of the less granular “error” or “success”. Here’s the new version:

mvcAjax = {};

mvcAjax.submit = mvcAjax.click = function(element, event, options)
{
    if (!event)
        event = window.event;

    if (event)
        $.event.fix(event).preventDefault();

    element = $(element).eq(0);
    isForm = element.is("form");
        
    $.ajax
    (
        {
            type: "POST",
            url: element.attr(isForm ? "action" : "href"),
            data: isForm ? element.serialize() : {},
            success: function(data)
            {
                if (options.onSuccess)
                    options.onSuccess(data);
            },
            error: function(request)
            {
                if (options.onError)
                    options.onError(request.status);
            },
            complete: function(request)
            {
                if (options.onComplete)
                    options.onComplete(request.status);
            },
            dataType: options.json ? "json" : undefined
        }
    );
};

And here’s the 408 byte (0.39% of the original 104,228 bytes) obfuscated and minified version:

mvcAjax={};mvcAjax.submit=mvcAjax.click=function(a,b,c){if(!b)b=window.event;b&&$.event.fix(b).preventDefault();a=$(a).eq(0);d=a.is("form");$.ajax({type:"POST",url:a.attr(d?"action":"href"),data:d?a.serialize():{},success:function(a){c.onSuccess&&c.onSuccess(a)},error:function(a){c.onError&&c.onError(a.status)},complete:function(a){c.onComplete&&c.onComplete(a.status)},dataType:c.json?"json":undefined})};

Google’s new Closure Tools helped identify a few additional optimizations.

For example:

if (flag)
{
    doSomething();
}

can be reduced to:

flag && doSomething();

You can download and compile the tools yourself or, more conveniently, use the web UI version.

2009 Nov 12 @ 11:53pm asp.net-mvc javascript jquery

Replacing MicrosoftMvcAjax.js

Update: I’ve written a follow-up, Even Smaller.

I’m a big fan of ASP.NET MVC. However, it requires a couple of bulky Javascript files to do its magic.

  • MicrosoftMvcAjax.js weighs in at 4,870 bytes.
  • It however utilizes a few functions from MicrosoftAjax.js, which is a whopping 99,358 bytes.

And those are the minified and obfuscated copies. Maybe I’m just jaded by ASP.NET WebForms, but I don’t entirely trust 100+ kilobytes of Microsoft Javascript to be included in every one of my pages.

A little research led me to Chris van de Steeg’s post about creating smaller methods that adhered to the same interface as MicrosoftMvcAjax.js’s methods so that they were still compatible with the AjaxHelper class in your Views.

The only point of this seemed to be to retain use of the AjaxHelper.BeginForm() method, which I can do without as it’s merely a convenience method to write an HTML form element.

Using the following call in your View:

using (Ajax.BeginForm("Create", "Widget", new AjaxOptions { OnSuccess = "onCreateWidgetSuccess" }) { }

…results in the following form element:

<form action="/Widget/Create" method="post" onsubmit="Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, onSuccess: Function.createDelegate(this, onCreateWidgetSuccess) });"></form>

Which relies on the Sys.Mvc namespace implemented by MicrosoftMvcAjax.js.

I decided to scrap the whole use of AjaxHelper, and created the following Javascript namespace and methods for simple Ajax form and link functionality to replace MicrosoftMvcAjax.js.

mvcAjax = {};

mvcAjax.submit = function(form, event, options)
{
	event = mvcAjax.fix(event);

	if (event)
		event.preventDefault();
	
	mvcAjax.request
	(
		$.extend
		(
			options,
			{
				url: $(form).attr("action"),
				data: $(form).serialize()
			}
		)
	);
};

mvcAjax.click = function(anchor, event, options)
{
	event = mvcAjax.fix(event);

	if (event)
		event.preventDefault();

	mvcAjax.request
	(
		$.extend
		(
			options,
			{
				url: $(anchor).attr("href"),
				data: {}
			}
		)
	);
}

mvcAjax.request = function(options)
{
	if (!options || !options.url)
		return;

	$.ajax
	(
		{
			type: "POST",
			url: options.url,
			data: options.data,
			success: function(data)
			{
				if (options.onSuccess)
					options.onSuccess(data);
			},
			error: function(request, status)
			{
				if (options.onError)
					options.onError(status);
			},
			complete: function(request, status)
			{
				if (options.onComplete)
					options.onComplete(status);
			},
			dataType: options.json ? "json" : undefined
		}
	);
};

mvcAjax.fix = function(event)
{
	if (!event)
		event = window.event;

	return event ? $.event.fix(event) : undefined;
};

Using it looks similar to the form element created by Ajax.Begin():

<form action="/Widget/Create" method="post" onsubmit="mvcAjax.submit(this, event, { onSuccess: onCreateWidgetSuccess, json: true })"></form>

It supports onSuccess, onError, and onComplete parameters that correspond to AjaxOption’s OnSuccess, OnFailure, and OnComplete properties.

It has the added feature of a json parameter that, when set to true, tells the underlying Ajax call to expect a JSON object to be returned.

Using the link version looks similar:

<a href="/Widgets/Clear" onclick="mvcAjax.click(this, event, { onSuccess: onClearWidgetsSuccess, json: true })">Clear Widgets</a>

The obfuscated and minified version below is a lean 668 bytes, 0.64% of the 104,228 byte combined size of the replaced files.

mvcAjax={};mvcAjax.submit=function(f,e,o){e=mvcAjax._f(e);if(e)e.preventDefault();mvcAjax._r($.extend(o,{u:$(f).attr("action"),d:$(f).serialize()}));};mvcAjax.click=function(a,e,o){e=mvcAjax._f(e);if(e)e.preventDefault();mvcAjax._r($.extend(o,{u:$(a).attr("href"),d:{}}));};mvcAjax._r=function(o){if(!o||!o.u)return;$.ajax({type:"POST",url:o.u,data:o.d,success:function(d){if(o.onSuccess)o.onSuccess(d);},error:function(r,s){if(o.onError)o.onError(s);},complete:function(r,s){if(o.onComplete)o.onComplete(s);},dataType:o.json?"json":undefined});};mvcAjax._f=function(e){if(!e)e=window.event;return e?$.event.fix(e):undefined;};
2009 Sep 28 @ 2:17pm netflix
[Netflix’s recommendation system] spreads demand across its inventory in an artificial way that obscures its customers’ actual preferences.
2009 Sep 25 @ 4:57pm privacy
2009 Sep 24 @ 2:03pm apple app store
The Apple App Store is a “flash in the pan” because it is a proprietary platform and … counter to consumers’ interests.
2009 Sep 24 @ 12:03pm productivity
2009 Sep 24 @ 10:00am mcdonald's consumerism
Between the tiny [South] Dakotan hamlets of Meadow and Glad Valley lies the McFarthest Spot: 107 miles distant from the nearest McDonald’s.
2009 Sep 23 @ 3:57pm
I’d rather offend people needlessly than use needless words, and you have to choose one or the other.