2007-01-01

Turbo dojo widgets

Background

In our project, we chose dojo as the AJAX toolkits, we use dojo widget programmatically, it's easy to use and easy to extend, but dojo's widget has big performance problem in our project.

In one page, we can have thousands of widgets, creating a widget in dojo is really slow, showing such a page needs like 20 minutes 100% CPU usage, it's far from acceptable, so I have to figure out some method to boost dojo dramatically.

dojo's widget is very powerful, you can define attach points and connect events in the templates, then dojo will parse the templates and pick up all the custom tags and process on them, after reading some dojo source code, I found out that the template parsing is the most time consuming part in widget creations, it takes like 80-90% of the whole time, so I decided to try to optimize this part first.

Every time creating a widget with dojo.widget.createWidget(), the template of that widget will be parsed, that is actually not needed, every kind of widget will have same template, so the result of the first parse can be cached, then it's not needed to parse the same template anymore.

I've seen some one in dojo's maillist talking about cloning a widget, I think it's the same idea I have here, but I don't have enough time to actually patch dojo to have a faster widget creation, so I first do some work to help our project running fast with dojo.

As I said before, our project create all the widgets in javascript, my turbo way only work in this scenario, if you want to use the widgets in html, this post can not help you.

My trick

common/turbo.js

dojo.provide("common.turbo");

dojo.require("dojo.widget.*");
dojo.require("dojo.widget.Manager");

_cachedWidgets = {}

common.turbo.createWidget = function(name, props, refNode, position){
var prototype = _cachedWidgets[name];
if (prototype == undefined){
prototype = dojo.widget.createWidget(name, props, refNode, position);
if (prototype["turboInit"] != undefined){
prototype.widgetType = name;
prototype.index = 0;
_cachedWidgets[name] = common.turbo.cloneWidget(prototype, {}, false);
prototype.turboInit();
}else{
//alert(name);
}
return prototype;
}
var result = common.turbo.cloneWidget(prototype, props, true);
if (refNode != null) refNode.parentNode.replaceChild(result.domNode, refNode);
return result;
}

common.turbo.cloneWidget = function(prototype, props, init){
var result = {};
for (k in prototype) result[k] = prototype[k];
for (k in props) result[k] = props[k];
var widgetId = props["id"];
result.index = ++prototype.index;
if (widgetId != undefined){
result.widgetId = widgetId;
}else{
result.widgetId = prototype.widgetType + "_" + result.index;
}
result.domNode = prototype.domNode.cloneNode(true);
if (init) result.turboInit();
dojo.widget.manager.add(result);
return result;
}

The logic here is simple, in our javascript code, we call common.turbo.createWidget() instead of dojo.widget.createWidget() to create a widget.

In common.turbo.createWidget(), it will call dojo.widget.createWidget() to create the first widget, so I don't need to write code to parse template and can keep using most of dojo's power(will talk about the limitation later), then after got the widget created, we check whether it has a method named as turboInit(), if it does, then means this widget is one of our turbo widgets, then we can use common.turbo.cloneWidget() to get a clone and cache it, then if the same type of widget is needed in the future, the cached prototype can be used to make another clone, then we make sure dojo's slow widget creation will only happen once per type, that is acceptable.

Here is an example of turboed widget

dojo.provide("common.widget.Button2");
dojo.widget.manager.registerWidgetPackage("common.widget");

dojo.require("dojo.widget.*");
dojo.require("dojo.widget.Button");

dojo.widget.defineWidget(
"common.widget.Button2",
dojo.widget.html.Button,
{
templateString:
'<div class="dojoButton" style="position:relative;">'
+ '
<div class="dojoButtonContents" align=center '
+ 'dojoAttachPoint="containerNode" '
+ 'style="position:absolute;'
+ 'z-index:2;"
></div>'
+ '
<img dojoAttachPoint="leftImage" '
+ 'style="position:absolute;left:0px;"
>'
+ '
<img dojoAttachPoint="centerImage" '
+ 'style="position:absolute;z-index:1;"
>'
+ '
<img dojoAttachPoint="rightImage" '
+ 'style="position:absolute;top:0px;right:0px;"
>'
+'</div
>',

turboInit: function(){
this.containerNode = this.domNode.childNodes[0];
this.leftImage = this.domNode.childNodes[1];
this.centerImage = this.domNode.childNodes[2];
this.rightImage = this.domNode.childNodes[3];

this.containerNode.innerHTML = this.caption;
this.sizeMyself();

dojo.event.connect(this.domNode, "onmouseup", this, this.onMouseUp);
dojo.event.connect(this.domNode, "onmousedown", this, this.onMouseDown);
dojo.event.connect(this.domNode, "onmouseover", this, this.onMouseOver);
dojo.event.connect(this.domNode, "onmouseout", this, this.onMouseOut);
dojo.event.connect(this.domNode, "onclick", this, this.buttonClick);
}

}
);

This is a subclass of dojo.widget.html.Button, in turboInit(), it rebuild the dojoAttachPoint from domNode, and attach events.

The usage of Button2 is same with other widget, just use common.turbo.createWidget() to create it.

By using this simple technology, our ajax page is pretty fast now, the same page now only takes like 20-30 seconds in widget creation part. Then with some lazy loading tech, our project is fast enough now.

The limitation of this approach

The dojoAttachPoint and dojoAttachEvent only works for the first widget, all cloned widgets needs to set them up in turboInit(), now for my own widget, I just don't use them in templates, in the case of changing a current widget like the example of Button2.js, I leave them in template and setup them up again in the turboInit() now.

And for better perforamnce, I didn't call all the functions dojo calls when create a new widget, e.g. postMixInProperties() and postCreate(), it's easy to call them in common.widget.cloneWidget(), while in our project it's not needed.

The simple clone in common.widget.cloneWidget() is not a deep one, I think this is better, you can choose deep copy some property of reinitialize it in turboInit().

Not all properties needs to be copied, actually some of them may have wrong value after the copy.

Future work

As I said, if I add the similiar hack to dojo's framework, then it's possible to just make all template parsing cached for all type of widgets, and don't need the turboInit() trick. This may or may not be easy, if I got time, will try to figure it out.