10. Optimizing

Jitsu is designed to take as much advantage of the compilation process as possible, in order to minimize the runtime processing and working set in the browser. This is critical for larger applications, since some browser's Javascript interpreters have only rudimentary garbage collection support - they are not intended for large in memory applications.

In addition, the Jitsu compiler and runtime provide several tools for further improving this. When you are ready to release your app, you will want to take advantage of these tools to get the best performance possible:

  1. Name Crunching
  2. Read-only Property Retrieval
  3. Template Serializing

Name Crunching

Name crunching compresses all javascript output by renaming all references to variable names. This yields both significantly smaller downloads, and better performance in some browsers.

Within a Jitsu app, the Jitsu Page Compiler can take JavaScript expressions with long identifier names like this:

    var myLongVariableName = this.aProperty.toUpperCase();

and generate code that looks like this:

    var a = this.b.toUpperCase();

We call this mechanism the name cruncher, or the cruncher for short.

Name crunching reduces script size, increasing download times, improving startup speed, and making the code execute faster on some browsers (notably IE6). Using the name cruncher also lets you use long descriptive names in your source code without worrying about download size. The name cruncher also makes it harder for people to reverse-engineer your site, which you may see as a positive or a negative consequence.

Due to various features of the JavaScript language, name crunching can never be completely automatic - there will always be a certain amount of pain writing Web applications that can work with a variable name cruncher. You can disable the Jitsu name cruncher to avoid this pain altogether (you can just use the whitespace stripper instead, for example). Alternatively, you can write apps that are designed to work with the cruncher.

If you want to use the cruncher, the following guidelines will help.

Turning on the name cruncher

In the jitsu task in a project's build.xml file, you turn the name cruncher using the crunchmode parameter:

    <jitsu outdir="." appdirname="app"
                baseurl="/jitsu/samples/dragdrop"
                crunchmode="longnames">
        <sources basedir="."> 
            <include name="*.src.html"/>            
            <include name="*.xml"/>            
        </sources>
    </jitsu>

Crunchmode can have four values: off, shortnames, longnames, and safenames:

CrunchModeDescription
off If crunchmode is "off", the cruncher leaves JavaScript variable names as-is in the generated code. This is the default.
shortnames If crunchmode is "shortnames", the cruncher uses the shortest possible variable names for each JavaScript variable - usually replacing variable names with one to three letter names.
longnames If crunchmode is "longnames", the cruncher appends a "_" suffix to every variable name that will be effected by crunchmode=shortnames.

For example, Control becomes Control_ and setValue becomes setValue_.

Variables which won't get crunched are left along, e.g. document and getElementById will not have an underscore.

This actually makes your project output slightly larger! But its very useful for debugging.

safenames If crunchmode is "safenames", the cruncher uses the same approach as "shortnames" but in addition adds a single letter prefix "W" to all generated names - helping to ensure that generated names don't clash with other non-crunched JavaScript code.

If you want to make your app crunchable, start by setting crunchmode to "longnames". Once everything is working there, switch to "shortnames" or "safenames".

Troubleshooting

After setting crunchmode to longnames, build and run the application. If everything works, congratulations - switch the mode to shortnames and try again. If not, here are some tips.

Avoid mixing string and dotted notation

If your code doesn't run when crunched, one likely culplit is that you are are mixing array and dot notation access for properties, i.e. you code that looks something like this:

    obj["counter"] = 1;   // using a string to index an associative array

    ...
    
    obj.counter = obj.counter + 1; // using JavaScript dot notation

The JavaScript language defines the notation a.b and a["b"] as being equivalent, so people often mix these two approaches.

When the Jitsu cruncher replaces variable names, it does not change strings. So after crunching, the above code looks something like:

    r2["counter"] = 1;  
     
    ...    
    r2.q = r2.q + 1;

This is clearly wrong. The first line is setting the "counter" entry, and the second line is incrementing the "q" entry.

The first step to solving this is to eliminate mixing these two approaches as much as possible: for any particular object, either always use the dot notation or always use the array notation, but not both together.

Your next approach is to adopt the IdManager.

The IdManager

IdManager provides a way to manage symbolic strings in a JavaScript application.

To use the IdManager, you first add something like this to the top of a script:

    IdManager.declare("ID_counter");    

This is roughly equivalent to writing:

    var ID_counter = "counter";

i.e. it causes a global variable called ID_counter to be created, with a string value that matches the name of the variable.

Then in your code, rather than using string constants like "counter" use ID_counter instead:

    obj[ID_counter] = 1;   // use an ID
    ...

You can use the IdManager.check() method to check that an ID is valid. For example:

    IdManager.check(aVar); // checks that aVar holds a valid ID

IdManager.check throws an exception if you pass it anything that is not a declared ID name.

Using the IdManager has two main advantages:

The IdManager and the Cruncher

The IdManager works together with the cruncher. When the cruncher changes variable names, it also changes the string assignments for all declared IDs in exactly the same way. In other words, the cruncher ensures that variable names and the strings in any corresponding IDs always correlate. If your script starts with:

    IdManager.declare("ID_someName");

Then ID_someName will be a string for which the following invariant holds:

    object[ID_someName] == object.someName

Here's what the cruncher does with a short example script:

Original CodeCrunched
IdManager.declare(
    "ID_gender", 
    "ID_male", 
    "ID_female");

var person = { gender: ID_female; }

if (person.gender == person[ID_gender])
    alert("gender");

if (person.gender == ID_female)
    alert("female");





var a = { b: "c"; }

if (a.b == a["b"]) 
    alert("gender");

if (a.b == "c")
    alert("female");

Function names

You've fixed all your array/dot notation issues, embraced the IdManager, and the code still doesn't run?

The next issue to look out for is code strings passed to methods like eval or setInterval.

For example you may have:

    function animateIt() { ... }
    setTimout("animateIt()", 1000);

The problem here is that the cruncher may change the name animateIt to some other name, producing:

    function e() { ... }
    setTimout("animateIt()", 1000);

Once again a variable changed by the string didn't.

You use the IdManager to resolve this issue as well, by changing the code to:

    // Declare an ID for the function we wish to refer to in a string
    IdManager.declare("ID_animateIt");
    
    function animateId() { ... }    
    setTimer(ID_animateIt + "()", 1000);    // Now this works with the cruncher

The cruncher now replaces both the animateIt function name and the ID_animateIt in the same way, so now the code works with the cruncher.

You must do the same work for code passed to eval and for code strings used for attributes like onclick or onload on an HTML node.

In general, its best to avoid code expressions in strings where possible.

External Names and #extern

If your code still isn't running, the next likely cause is that the cruncher is changing the variable name of something it shouldn't.

To detect this, look at the code with the crunchmode="longnames". In this crunch mode, instead of crunching variables to one or two letters, the cruncher appends a "_" to the end of all the variables its planning to rename. This makes it easy to spot of the cruncher is renaming a variable it shouldn't.

For example, when we first added a setOpacity function to the framework, the code worked great until we turned on the cruncher. Looking at the code with crunchmode="longnames", we saw that our setOpacity function looked as follows:

XPlatform_.setOpacity_ = function setOpacity_(element__, opacity_) {
    if (element_.filters_) {
        try {
            element_.filters_.alpha_.opacity_ = opacity_ * 100;
        } catch (e) { }
    } else if (element_.style.MozOpacity_ !== (void 0)) {
        var val_ = parseFloat(opacity_);
        if (val_ > .99) val_ = .99;
        element_.style.MozOpacity_ = val_;
    } else if (element_.style.opacity_ !== (void 0))
        element_.style.opacity_=opacity_;     
}

The cruncher has modified the names "opacity_", "alpha_", "filters_" and "MozOpacity_" - but these are all names that are part of the web browser's API, so we need to tell the cruncher to leave those names alone.

The #extern directive does this. We added a #extern() line to the top of the setOpacity function in the source code and it fixed the problem:

XPlatform.setOpacity = function setOpacity(element, opacity) {

    #extern(filters,alpha,opacity,MozOpacity);
    
    if (element.filters) {
        ... 
    

The #extern line at the start of the function tells the cruncher not to crunch the listed names.

Note that this effects the name throughout the entire project. Once you declare a variable name as #extern the cruncher will never shorten that variable in any files in the project.

Making reusable crunchable libraries

You may want to create a JavaScript library that works both with Jitsu crunched apps and standalone.

To do this, you can add a minimal IdManager class to your application:

// Minimal IdManager implementation
if (!IdManager) {
function IdManager() { }
IdManager.declare = function() { 
    for (var a = arguments, i = 0; i < a.length; i++) self[a[i]] = a[i].substr(2); 
}
IdManager.check = function(id) { 
    var valid = (typeof(id) == 'string' && self[id]); 
    if (!valid) throw "Invalid ID (misspelt name or missing using?)"; 
}
}

If you place these three at the top of your library, you can then call IdManager.declare and IdManager.check in your code as appropriate, and have code that runs both with the cruncher and standalone.

Read-only Property Retrieval

The #bind operator is a powerful construct for bidirectional, synchronous databinding. It allows you to change a property's value in either data or a control, and have it update the corresponding bound value in the object its bound to. It also eliminates the timing problems involved in wiring objects up - bindings automatically connect when there is a full path between the source and destination.

This power comes at a cost, however. When you create a binding, it creates bidirectional listener references between the source and destination. It also creates a sequence of objects that thread along the path between the two objects, called binding beads. If you have alot of bindings, especially in repeating lists, your page may become slow.

Further, there are a great many situations where data is static and read-only, and does not need bidirectional synchronization. To take advantage of these cases, Jitsu provides an alternative to #bind, called #get.

The #get expression part has similar semantics to #bind. It can use paths, and can have convert expressions and defaultValues. When it is used, however, the data is retrieved immediately when the markup's underlying objects are instantiated, and never retrieved again. This has two important implications:

  1. The data must be read only, and static.
  2. The data must be available at the time of instantation. In general, this means it must be defined in the context of a template. [TODO - should we fold templates and binding containers together?]

It is highly recommended that you use #get where possible, in list-intensive applications in particular.

Template Serialization

When generating a page, the Jitsu compiler generates functions to create the controls declared in markup. These functions are referred to as templates, and are executed at runtime to instantiate your page. When these execute, a control tree is created. This can lead to a fair amount of processing, involving the threading of bindings, calls to setValue(), and possibly redundant property sets for subclassed controls.

To minimize this, Jitsu offers a template serializer. What it does is execute the template at compile time to create an instance of the control tree. It then replaces the existing template function with a new one, containing code that represents the control tree, with all of its initialized state, serialized to javascript. This serialization code is similar to JSON, except that it contains constructors.

To illustrate, consider the following page markup:

    <html xmlns="http://www.w3.org/1999/xhtml"
        xmlns:j="http://www.jitsu.org/schemas/2006">

        <body>
            My first application:

            <j:App>
                Hello World!
            </j:App>        
        </body>

    </html>

Without template serialization, the creation function looks like this:

function initializeApp0(target, resourceOwner, data) {
	target.setInitialValues(
		"childControls", new SitedList(' Hello World! '));
};

After template serialization, the creation function looks like this:

  function initializeApp0(target,resourceOwner,data) {
    var o0=new SitedList();
    o0.internalArray=[' Hello World! '];
    target.setInitialValues('childControls', o0);
}

In the serialized version, the SitedList was created, and its internal properties were set as simple javascript property sets. This may not seem like much difference. Consider a larger example. Here's the markup for a template that represents a list item:

        <j:template>
            <!-- the button bubbles an "selectItem" event-->
            <tr class="getRowClass(#bind(item.index), #bind(isSelected))">
                <td><j:CheckBox checked="#bind(isMultiSelected)"/></td>
                <td><j:Button bubbleEvent="selectItem"
                        text="#bind(item.lastName)" isLink="true"/></td>
                <td><j:Label text="#get(item.firstName)"/></td>
                <td><j:Label text="#get(item.birthYear)"/></td>
                <td><j:Label text="#get(item.deathYear, convert:(val == -1 ? '-' : val))"/></td>
                <td><j:Label text="#get(item.imageUrl, convert:(val == '' ? 'no' : 'yes' ))"/></td>
                <td><j:Label text="#get(item.position)"/></td>
            </tr>
        </j:template>
Here's the code for that template:
function createTemplate2(resourceOwner, item, index) {
	var o, o2, o3;
	var sp = self.sp;
	
	// Create ElementControl for <tr>: (line: 155)
	o = new ElementControl('tr');
	
	// Create <CheckBox>: (line: 157)
	o2 = new CheckBoxControl();
	o2.addBinding("checked", sp.b10);
	
	// Create <Button ID="Button1">: (line: 158)
	o3 = new ButtonControl();
	o3.addBinding("text", sp.b11);
	
	o3.setInitialValues(
		"isLink", true, 
		"bubbleEvent", "selectItem");
	o.addCalculatedBinding("@class", [sp.b12, sp.b13], cbv2);
	
	o.setInitialValues(
		"isBindingContainer", true, 
		"childControls", new SitedList('<td>:', o2, '</td>:<td>:', o3, '</td>:<td>:', 
		ensureString(item.getValue("firstName")), '</td>:<td>:', ensureString(item.getValue("birthYear")), 
		'</td>:<td>:', ensureString(line162_Label_text_convert(item.getValue("deathYear"))), 
		'</td>:<td>:', ensureString(line163_Label_text_convert(item.getValue("imageUrl"))), 
		'</td>:<td>:', ensureString(item.getValue("position")), '</td>:'));
	return o;
};
And here's the serialized code for that template:
function createTemplate2(resourceOwner,item,index) {
    var sp = self.sp;
    var o0=new ElementControl();
    o0.tagName='tr';
        var o1=new SitedList();
            var o2=new CalculatedBinding();
            o2.propertyId='@class';
            o2.calculationFunction=cbv2;
                var o3=new Binding();
                o3.sinkPropertyKey='a0';
                o3.path=sp.b12;
                o3.sinkObject=o2;
                var o4=new Binding();
                o4.sinkPropertyKey='a1';
                o4.path=sp.b13;
                o4.sinkObject=o2;
            o2.bindings=[o3,o4];
                var o5=new BindingBead();
                o5.binding=o3;
                o5.position=0;
                var o6=new BindingBead();
                o6.binding=o4;
                o6.position=0;
            o2.bindingBeads=[o5,o6];
            o2.site=o1;
        o1.internalArray=[o2];
            var o7=new BindingBead();
            o7.binding=o3;
            o7.position=0;
            var o8=new BindingBead();
            o8.binding=o4;
            o8.position=0;
        o1.bindingBeads=[o7,o8];
        o1.site=o0;
    o0.calculatedBindings=o1;
    o0.isBindingContainer=true;
        var o9=new BindingBead();
        o9.binding=o3;
        o9.position=0;
        var o10=new BindingBead();
        o10.binding=o4;
        o10.position=0;
        var o11=new BindingBead();
            var o12=new Binding();
            o12.sinkPropertyKey='text';
            o12.path=sp.b11;
                var o13=new ButtonControl();
                o13.bindings=[o12];
                    var o14=new BindingBead();
                    o14.binding=o12;
                    o14.position=0;
                o13.bindingBeads=[o14];
                o13.isLink=true;
                o13.bubbleEvent='selectItem';
                    var o15=new SitedList();
                        var o16=new CheckBoxControl();
                            var o17=new Binding();
                            o17.sinkPropertyKey='checked';
                            o17.path=sp.b10;
                            o17.sinkObject=o16;
                            o17.sourceObject=o0;
                            o17.live=true;
                        o16.bindings=[o17];
                            var o18=new BindingBead();
                            o18.binding=o17;
                            o18.position=0;
                        o16.bindingBeads=[o18];
                        o16.site=o15;
                    o15.internalArray=['',o16,'',o13,'',ensureString(item.getValue('firstName')),'',ensureString(item.getValue('birthYear')),'',ensureString(line162_Label_text_convert(item.getValue('deathYear'))),'',ensureString(line163_Label_text_convert(item.getValue('imageUrl'))),'',ensureString(item.getValue('position')),''];
                        var o19=new BindingBead();
                        o19.binding=o17;
                        o19.position=0;
                        var o20=new BindingBead();
                        o20.binding=o12;
                        o20.position=0;
                    o15.bindingBeads=[o19,o20];
                    o15.site=o0;
                        var o21=new Object();
                            var o22=new Delegate();
                            o22.obj=o0;
                            o22.methodFunction=ContainerControl.prototype.childListChanged;
                        o21.listChanged=[o22];
                    o15.allEventHandlers=o21;
                o13.site=o15;
            o12.sinkObject=o13;
        o11.binding=o12;
        o11.position=1;
    o0.bindingBeads=[o9,o10,o11];
    o0.childControls=o15;
    o0.sourceBindings=[o17];
    return o0;
}

At this point you may be thinking, "but that just expanded the method like crazy. The download has just increased, and the number of statements executed is much larger." Before you decide to ignore this feature, consider a few points:

  1. You're looking at an uncrunched version of the code; a crunched version will be considerably smaller.
  2. These javascript files are downloaded once, and cached, so size isn't quite so important.
  3. The amount of processing that has been eliminated is significant.

But the proof is in the benchmarks. In this example, we've found template serialization to improve load time on both IE and Firefox by over 20%. For subclassed controls, it would probably be even more.

Invoking the Template Serializer

By default, the template serializer is turned off. To turn it on, set the flag serializeTemplate='true'