[philiptellis] /bb|[^b]{2}/
Never stop Grokking


Showing posts with label dynamic script node. Show all posts
Showing posts with label dynamic script node. Show all posts

Thursday, January 07, 2010

Handling document.write in dynamic script nodes

As a performance junkie, I generally want my pages to be as fast as I can make them. When I control the entire page, that typically means going all out. As a web user, I get annoyed when the little spinner on my browser keeps spinning even though I know that all the essential content on my page has loaded. When I redesigned my website late last year, I decided to address this issue.

Now if you take a look at my homepage, you'll notice a bunch of external resources:
  1. My Yahoo! Avatar
  2. My twitter feed
  3. My delicious bookmarks
  4. My upcoming events
  5. My dopplr badge
  6. My flickr photos
That's resources from six services whose performance I cannot control, nor rely upon. They're also six components on my page that aren't critical to the content of my page, and if one of them were unavailable for some amount of time, that wouldn't really hurt the purpose of the page.

Now I've been working with dynamic script nodes for a very long time to do background loading of scripts, but in all those cases, those scripts played nicely with other things on the page, and had JSONP responses. Not all the resources that I use now have this behaviour, so I had to come up with something else. Let's go through them one at a time.

To start with, I just included the javascript that all these services told me to include. Since the Avatar is just an image, I just used an img tag and left it at that. I've also never seen any performance issues with my Y! Avatar. The other services, however, all went down at some point or the other, and all had to be included as javascript.

Twitter

I started with the twitter widgets page. I copied the code, and pasted it where I wanted the twitter widget to show up. It's a lot of code, but that was okay to start with:
<script src="http://widgets.twimg.com/j/2/widget.js"></script>
<script>
new TWTR.Widget({
  version: 2,
  type: 'profile',
  rpp: 4,
  interval: 6000,
  width: 250,
  height: 300,
  theme: {
    shell: {
      background: '#333333',
      color: '#ffffff'
    },
    tweets: {
      background: '#000000',
      color: '#ffffff',
      links: '#4aed05'
    }
  },
  features: {
    scrollbar: false,
    loop: false,
    live: false,
    hashtags: true,
    timestamp: true,
    avatars: false,
    behavior: 'all'
  }
}).render().setUser('bluesmoon').start();
</script>
I then had to figure out if I could easily move the code to the bottom of my document so that it didn't block the rest of my page's load. twitter tends to go down more often than any of the other services.

I read through the source code for widget.js and found out that it creates a DIV into which it writes itself, however, you can create the DIV yourself, and pass its id to the widget constructor. The new code becomes:
<script src="http://widgets.twimg.com/j/2/widget.js"></script>
<script>
new TWTR.Widget({
  version: 2,
  type: 'profile',
  rpp: 4,
  id: 'twitter_widget',
  interval: 6000,
  width: 250,
  height: 300,
  theme: {
    shell: {
      background: '#333333',
      color: '#ffffff'
    },
    tweets: {
      background: '#000000',
      color: '#ffffff',
      links: '#4aed05'
    }
  },
  features: {
    scrollbar: false,
    loop: false,
    live: false,
    hashtags: true,
    timestamp: true,
    avatars: false,
    behavior: 'all'
  }
}).render().setUser('bluesmoon').start();
</script>
I could then create a DIV with an id of twitter_widget where I wanted the widget to go, and push the twitter code to the bottom of my page. This worked well. Kudos to Dustin Diaz for building a flexible widget, but really, you need to make those API docs available somewhere in that widget.

Anyway, we'll get back to twitter later, let's move on.

delicious

Finding the delicious badge was the toughest part. It's hidden on the help page under tools. Anyway, if you don't want to search for it, this is the link for delicious linkrolls.

After configuring the widget, I ended up with this javascript:
(I've split it onto multiple lines for readability)
<script type="text/javascript"
    src="http://feeds.delicious.com/v2/js/bluesmoon?
         title=My%20Delicious%20Bookmarks
         &icon=s
         &count=5
         &bullet=%E2%80%A2
         &sort=date
         &name
         &showadd"></script>
This code is not very nice, because I can't just move it elsewhere in the document. Looking at the page source, it seems that it initialises a Delicious namespace, and then loads two other javascript files. The first handles rendering of the linkroll, and the second is a JSONP feed of my links. Unfortunately, the first script chooses to write to the document using the document.write() javascript function.

Note that this function is the primary reason for javascript blocking page rendering -- it can modify the structure of the page as it loads. I decided to tackle this later.

Upcoming

Upcoming's badges are linked to from the page footer, so it was easy to start with this. I got the code, but decided to style it myself. The code points to the following javascript file (again wrapped for readability):
http://badge.upcoming.yahoo.com/v1/?
    badge_type=user
    &badge_size=sm
    &badge_layout=v
    &badge_styling=2
    &badge_no_background=
    &badge_venue=1
    &date_format_type=us_med
    &id=54783
The source of this badge shows that it also uses document.write() to write itself into the document. Solving this problem would tackle upcoming and delicious as well.

Dopplr

Dopplr was next, and was by far the easiest to work with. The account section points to a Blog badge which gives you a bunch of javascript to include onto your page among other things. The javascript link for my account is:
http://www.dopplr.com/blogbadge/script/6d1f4effa8fc5ac6db60160860ece8be?
    div-id=dopplr-blog-badge-for-bluesmoon
And the source code of that script had a lot of comments saying exactly what you can do with it. Brilliant. I just created a DIV with an id of my choice, and pushed this script to the bottom of the page.

Flickr

After the Avatar, delicious and upcoming, Flickr was the fourth Yahoo! property on my page. The previous two had already proved bad players, so my hopes weren't too high. Still, flickr has been good in the past, so I looked into it. The flickr badge page has a wizard to create the badge for you. This got me the following javascript:
http://www.flickr.com/badge_code_v2.gne?
    count=10
    &display=random
    &size=s
    &layout=x
    &source=user
    &user=57155801%40N00
Looking at the source of that showed the same old problem. document.write()

It was time to tackle this beast.

document.write()

To handle document.write(), I had to redefine what it did so that it would work asynchronously even after the entire page had loaded. I came up with this javascript:
document.writeln = document.write = function(s) {
 var id='';
 if(s.match(/\bupcoming_badge/)) {
  id='upb_events';
 }
 else if(s.match(/\bflickr_badge_image\b/)) {
  id='flickr_badge_wrapper';
 }
 else if(s.match(/\bdelicious\b/)) {
  id='delicious_widget';
 }
 else {
  id='overflow_div';
 }

 document.getElementById(id).innerHTML = s;
 return true;
};
It checks the content of the HTML to be written, and if it matches one of the three badges that I expect, I write it into the innerHTML of a DIV that I've already created for that badge. If it's something I don't recognise, then I assume that it doesn't matter where on the page it shows up, so just write it to an off-screen DIV.

I also had to make sure that there was no javascript being written out -- which is what the delicious badge was doing. In that case, I included the resulting javascript instead of including the javascript that the badge gave me.

Asynchronous loading

This worked, except that badges were still loaded in the order that I included the javascript, and if one blocked, all the others would wait, so I needed to make things asynchronous. This was easily accomplished using dynamic script nodes and handling the onreadystatechange event.

With this solved, I decided to parallelise downloads by moving all the code back into the head of the document. That way the script nodes would download in parallel with the document. Unfortunately, that also meant that some scripts might load up before the DIVs they need are available. Only dopplr handled this case well. For all the others, I had to handle it.

I ended up changing the write function above to defer until the required DIV was available.

Rather than include the entire Javascript here, I'll point you to the source file that you can see for yourself. It's not commented, but it's fairly small, so if you already know javascript, it should be easy to read.

With that, I had five badges on my page, all loaded asynchronously, and in parallel with each other and the page itself. My entire page is now reduced to four synchronous components: The HTML, CSS, Javascript and Avatar image (and possibly the favicon). Everything else is loaded asynchronously in the background and affects neither the firing of my onload handler, nor the browser's spinner.

You can see it in action on bluesmoon.info. Go ahead and view source.

Short URL: http://tr.im/docwritedynscr

Sunday, October 08, 2006

Unintrusive dynamic script nodes

There are several methods of doing remoting via javascript. Dynamic script nodes are one of the easiest to use. Unlike XHR, which requires a response handler for most cases, dynamic script nodes require no javascript action after the initial call. The response from the server contains all logic that needs to be executed, and the browser takes care of executing it without interrupting the rest of your control flow.

They do come with two caveats however.

Dynamic script nodes also allow one to do cross-domain remoting without setting off sirens and flashing lights in the browser. This opens up an inherrent security problem. If you - as the developer of this application - do not have control over the source of your script, then you cannot trust that it will do nothing malicious. I'll skirt the issue in this article which concentrates on issue number two.

Using dynamic script nodes involves adding a <script> node to the DOM. For a simple application that just makes one or two calls, this isn't much of an issue, but for a complex application, the DOM can easily grow large, which starts to push memory limits of browsers.

An application that makes use of the flickr APIs or Yahoo! APIs that return JSON data suitably wrapped in a callback of your choice could hit these limits if built entirely in Javascript.

The call would be something like this:
var scr = document.createElement("script");
scr.type="text/javascript";
scr.src = "http://api.flickr.com/services/rest/?method=flickr.interestingness.getList"
+ "&api_key=xxxxxxxxx&format=json&jsoncallback=myfunc";
document.body.appendChild(scr);
And a large number of such calls will add a lot of <script> nodes to the document, all of which have already served their purpose and are no longer needed.

These script nodes can be safely removed as soon as they've called their callback function. One needn't even wait for the callback to return, which means that the removal could be done within the callback itself.

While simple on the face of it, there's a bunch of housekeeping that goes with making this possible, and this isn't logic that all developers should be required to code into all their callbacks. It really should be generic enough to be separated out.

The code I came up with looks like this:
// The global callbacks array that stores a reference to all
// active callbacks.  This array is sparse.
var callbacks = [];

function call_api_method(url, callback)
{
  // We create the script element first so that it will
  // be available to the closure
  var scr = document.createElement("script");

  // Now add our custom callback to the callback array
  // so that the added script node can access it
  var i = callbacks.length;
  callbacks[i] = function(json)
  {
    // first remove the script node since we no longer
    // need it
    document.body.removeChild(scr);

    // On failure display a generic error message
    if(json.stat == 'fail')
      alert("Error calling method: " + json.errorString);
    // On success, call the callback
    else
      callback(json);

    // Clear out our entry in the callback array since we
    // don't need it anymore
    callbacks[i] = null;
    delete callbacks[i];

    // and clear out all outer variables referenced in the
    // closure to prevent memory leaks in some browsers
    i = scr = callback = null;
  };

  scr.type="text/javascript";
  // add our own callback function to the script url
  // the resource sitting at the other end of this url
  // needs to know what to do with this argument
  scr.src = url
    + '&callback=' + encodeURIComponent('callbacks["' + i + '"]');

  // finally, add the script node to initiate data transfer
  document.body.appendChild(scr);
}
A few things stand out:
  1. The reference to the script node is held by the callback since it's a closure
  2. A global reference to the callback is necessary so that the script can access it
  3. We need to clean up in a particular order to avoid memory leaks caused by circular references
The comments in the code should explain the logic.

This code is moderately simplified from what it would be if it were truly generic. Additions that would need to be made include:
  • Encapsulate the callbacks array and the method into an object so that we don't pollute global space.
  • Instead of accepting a callback function, accept a callback object with separate methods for success and failure as well as callback arguments and execution scope.
  • For most applications, the url would be similar for various calls differring only in small parts, eg: the method name in the Flickr API. It would be good if the above class were an abstract base class with specialisations for different services providing the base API url.
Add your own ideas in the comments, as well as specific uses.

This is what I did for the flickr API:
var apikey = "xxxxxxxxx";
var apiurl = "http://api.flickr.com/services/rest?api_key=" + apikey + "&format=json&method=flickr.";
var usernamemap = {};

function call_api_method(method, uname, callback, params)
{
  var scr = document.createElement("script");

  var cb = function(json)
  {
    document.body.removeChild(scr);
    usernamemap[uname].callback = null;

    if(json.stat == 'fail')
      alert("Error " + json.code + " calling flickr." + method + ": " + json.message);
    else
      callback(json, uname);

    scr = uname = method = callback = null;
  };

  usernamemap[uname] = usernamemap[uname] || {};
  usernamemap[uname].callback = cb;

  scr.type="text/javascript";

  var url = apiurl + method
      + "&jsoncallback=usernamemap" + encodeURIComponent("['" + uname + "']") + ".callback";
  for(var k in params)
    if(params.hasOwnProperty(k))
      url += '&' + encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);


  scr.src = url;

  document.body.appendChild(scr);
}

And this is how you call methods on flickr:
call_api_method("people.findByUsername", uname, map_user_to_nsid, {"username":uname});

call_api_method("photosets.getList", uname, show_set_list, {"user_id":nsid});


PS: I didn't mention it earlier, but caveat number three is the maximum size restriction on GET requests. There's nothing that can be done to get around this since the browser is going to make a GET request. Just use this for API calls that don't send too much data to the server. You can receive as much as you want.

...===...