Everything tagged code (2 posts)

Sencha Con 2013: Fastbook

I didn't plan on writing a post purely on Fastbook, but Jacky's presentation just now was so good I felt it needed one. If you haven't seen Fastbook yet, it is Sencha's answer to the (over reported) comments by Zuckerburg that using HTML5 for Facebook's mobile app was a mistake.

After those comments there was a lot of debate around whether HTML5 is ready for the big time. Plenty of opinions were thrown around, but not all based on evidence. Jacky was curious about why Facebook's old app was so slow, and wondered if he could use the same technologies to achieve a much better result. To say he was successful would be a spectacular understatement - Fastbook absolutely flies.

Performance can be hard to describe in words, so Sencha released this video that demonstrates the HTML5 Fastbook app against the new native Facebook apps. As you can see, not only is the HTML5 version at least as fast and fluid as the native versions, in several cases it's actually significantly better (especially on Android).

Fastbook

Challenges

The biggest challenge here is dynamically loading and scrolling large quantities of data while presenting a 60fps experience to the user. 60fps means you have just 16.7ms per frame to do everything, which is a hugely tall order on a CPU and memory constrained mobile device.

The way to achieve this is to treat the page as an app rather than a traditional web page. This means we need to be a lot more proactive in managing how and when things are rendered - something that traditionally has been in the domain of the browser's own rendering and layout engines. Thankfully, the framework will do all of this for you.

As an example, Jacky loaded up Gmail's web app and showed what happens when you scroll a long way down your inbox. The more you scroll, the more divs are added to the document (one new div per message). Each div contains a bunch of child elements too, so we're adding maybe a dozen or so nodes to our DOM tree per message.

The problem with this is that as the DOM tree gets larger and larger, everything slows down. You could see the inspector showing slower and slower layout recalculations, making the app sluggish.

The solution is to recycle DOM nodes once they're no longer visible. In this way, a list that seems to have infinite content could contain only say 10 elements - just enough to fill the screen. Once you scroll down the list, DOM nodes that scrolled off the top are detached, updated with new data and placed at the bottom of the list. Simple. Ingenius. Beautiful.

Prioritization

There's usually a lot more going on in an app than just animating a scrolling view though. There's data to load via AJAX, images to load, compositing, processing, and whatever else your app needs to do. And then there are touch events, which need to feel perfectly responsive at all times, even while all of this is going on.

To make this sane and manageable, we have a new class called AnimationQueue. All of the jobs I just mentioned above - handling touch events, animation, network requests and so on - are dispatched through the AnimationQueue with a given priority. Touch event handling has the top priority, followed by animation, followed by everything else.

AnimationQueue does as much as it can in that 16.7ms window, then breaks execution again to allow the browser to reflow/repaint/whatever else it needs to do. What this means is that while scrolling down a large list, it's likely that our CPU/GPU is being taxed so much that we don't have any time to load images or other low priority jobs.

This is a Good Thing, because if we're scrolling through a large list there's a good chance we are going to skip right over those images anyway. In the end they're loaded as soon as the AnimationQueue has some spare time, which is normally when your scrolling of the list has slowed down or stopped.

Sandboxing

The final, and most complex technique Jacky discussed was Sandboxing. The larger your application gets, the larger the DOM tree. Even if you are using best practices, there's an expense to simply having so many components on the same page. The bottleneck here is in the browser itself - looks like we need another hack.

To get around this, we can dynamically create iframes that contain parts of our DOM tree. This way our main page DOM tree can remain small but we can still have a huge application. This not only speeds up browser repaint and reflow, it also improves compositing performance, DOM querying and more.

This all happens under the covers and Jacky's aiming on including Ext.Sandbox in Sencha Touch 2.3 so that all apps can take advantage of this huge improvement. He cautioned (rightly) that it'll only make 2.3 if it's up to his high standards though, so watch this space.

Continue reading

Writing Better JavaScript - split up long methods

For the second time this week I'm going to pick on the usually delightful Ext JS library. Last time we discussed the overzealous use of the Module pattern; this time it's the turn of bloated methods.

As before, I'm not really picking on Ext at all - this happens all over the place. But again, this is the library closest to my heart and the one I know the best.

The Problem

We're going to take a look at Ext.data.XmlReader's readRecords method. Before we get started though, I'll repeat that this is intended as an example of an approach, not a whine at Ext in particular.

/**
* Create a data block containing Ext.data.Records from an XML document.
* @param {Object} doc A parsed XML document.
* @return {Object} records A data block which is used by an {@link Ext.data.Store} as
* a cache of Ext.data.Records.
*/
readRecords: function(doc) {
/**
* After any data loads/reads, the raw XML Document is available for further custom processing.
* @type XMLDocument
*/
this.xmlData = doc;
var root = doc.documentElement || doc;
var q = Ext.DomQuery;
var recordType = this.recordType, fields = recordType.prototype.fields;
var sid = this.meta.idPath || this.meta.id;
var totalRecords = 0, success = true;
if(this.meta.totalRecords){
totalRecords = q.selectNumber(this.meta.totalRecords, root, 0);
}

if(this.meta.success){
var sv = q.selectValue(this.meta.success, root, true);
success = sv !== false && sv !== 'false';
}
var records = [];
var ns = q.select(this.meta.record, root);
for(var i = 0, len = ns.length; i < len; i++) {
var n = ns[i];
var values = {};
var id = sid ? q.selectValue(sid, n) : undefined;
for(var j = 0, jlen = fields.length; j < jlen; j++){
var f = fields.items[j];
var v = q.selectValue(Ext.value(f.mapping, f.name, true), n, f.defaultValue);
v = f.convert(v, n);
values[f.name] = v;
}
var record = new recordType(values, id);
record.node = n;
records[records.length] = record;
}

return {
success : success,
records : records,
totalRecords : totalRecords || records.length
};
}
/**
* Create a data block containing Ext.data.Records from an XML document.
* @param {Object} doc A parsed XML document.
* @return {Object} records A data block which is used by an {@link Ext.data.Store} as
* a cache of Ext.data.Records.
*/
readRecords: function(doc) {
/**
* After any data loads/reads, the raw XML Document is available for further custom processing.
* @type XMLDocument
*/
this.xmlData = doc;
var root = doc.documentElement || doc;
var q = Ext.DomQuery;
var recordType = this.recordType, fields = recordType.prototype.fields;
var sid = this.meta.idPath || this.meta.id;
var totalRecords = 0, success = true;
if(this.meta.totalRecords){
totalRecords = q.selectNumber(this.meta.totalRecords, root, 0);
}

if(this.meta.success){
var sv = q.selectValue(this.meta.success, root, true);
success = sv !== false && sv !== 'false';
}
var records = [];
var ns = q.select(this.meta.record, root);
for(var i = 0, len = ns.length; i < len; i++) {
var n = ns[i];
var values = {};
var id = sid ? q.selectValue(sid, n) : undefined;
for(var j = 0, jlen = fields.length; j < jlen; j++){
var f = fields.items[j];
var v = q.selectValue(Ext.value(f.mapping, f.name, true), n, f.defaultValue);
v = f.convert(v, n);
values[f.name] = v;
}
var record = new recordType(values, id);
record.node = n;
records[records.length] = record;
}

return {
success : success,
records : records,
totalRecords : totalRecords || records.length
};
}

Anyone care to tell me what this actually does? Personally, I have absolutely no idea. I recently found myself needing to implement an XmlReader subclass with a twist which required understanding how this works, and let's just say it wasn't easy!

So what is it that makes the above so terrifyingly hard to understand? Well, in no particular order:

  • It's too long - you'd need to be a genius to easily understand what's going on here
  • The variable names don't make much sense - some of the oddest include 'q', 'ns', 'v', 'f' and 'sv'
  • There's minimal commenting - we're given a single-line clue at the very top as to what these 40-odd lines do

A Solution

Let's see how the reworked code below addresses each of the concerns above:

  • Although we end up with more lines of code here, no single method is more than around 10 LOC
  • No single letter variable names - you no longer have to decode what 'sv' means
  • Constructive commenting allows rapid comprehension by skimming the text

One additional and enormous benefit here comes directly from splitting logic into discrete methods. Previously if you'd wanted to implement your own logic to determine success, get the total number of records or even build a record from an XML node you'd be stuck. There was no way to selectively override that logic without redefining that entire monster method.

With our new approach this becomes trivial:

Ext.extend(Ext.data.XmlReader, Ext.data.DataReader, {
readRecords: function(doc) {
this.xmlData = doc;

//get local references to frequently used variables
var root = doc.documentElement || doc,
records = [],
nodes = Ext.DomQuery.select(this.meta.record, root);

//build an Ext.data.Record instance for each node
Ext.each(nodes, function(node) {
records.push(this.buildRecordForNode(node));
}, this);

return {
records : records,
success : this.wasSuccessful(root),
totalRecords: this.getTotalRecords(root) || records.length
};
},

/**
* Returns a new Ext.data.Record instance using data from a given XML node
* @param {Element} node The XML node to extract Record values from
* @return {Ext.data.Record} The record instance
*/
buildRecordForNode: function(node) {
var domQuery = Ext.DomQuery,
idPath = this.meta.idPath || this.meta.id,
id = idPath ? domQuery.selectValue(idPath, node) : undefined;

var record = new this.recordType({}, id);
record.node = node;

//iterate over each field in our record, find it in the XML node and convert it
record.fields.each(function(field) {
var mapping = Ext.value(field.mapping, field.name, true),
rawValue = domQuery.selectValue(mapping, node, field.defaultValue),
value = field.convert(rawValue, node);

record.set(field.name, value);
});

return record;
},

/**
* Returns the total number of records indicated by the server response
* @param {XMLDocument} root The XML response root node
* @return {Number} total records
*/
getTotalRecords: function(root) {
var metaTotal = this.meta.totalRecords;

return metaTotal == undefined
? 0
: Ext.DomQuery.selectNumber(metaTotal, root, 0);
},

/**
* Returns true if the response document includes the expected success property
* @param {XMLDocument} root The XML document root node
* @return {Boolean} True if the XML response was successful
*/
wasSuccessful: function(root) {
var metaSuccess = this.meta.success;

//return true for any response except 'false'
if (metaSuccess == undefined) {
return true;
} else {
var successValue = Ext.DomQuery.selectValue(metaSuccess, root, true);
return successValue !== false && successValue !== 'false';
}
}
});
Ext.extend(Ext.data.XmlReader, Ext.data.DataReader, {
readRecords: function(doc) {
this.xmlData = doc;

//get local references to frequently used variables
var root = doc.documentElement || doc,
records = [],
nodes = Ext.DomQuery.select(this.meta.record, root);

//build an Ext.data.Record instance for each node
Ext.each(nodes, function(node) {
records.push(this.buildRecordForNode(node));
}, this);

return {
records : records,
success : this.wasSuccessful(root),
totalRecords: this.getTotalRecords(root) || records.length
};
},

/**
* Returns a new Ext.data.Record instance using data from a given XML node
* @param {Element} node The XML node to extract Record values from
* @return {Ext.data.Record} The record instance
*/
buildRecordForNode: function(node) {
var domQuery = Ext.DomQuery,
idPath = this.meta.idPath || this.meta.id,
id = idPath ? domQuery.selectValue(idPath, node) : undefined;

var record = new this.recordType({}, id);
record.node = node;

//iterate over each field in our record, find it in the XML node and convert it
record.fields.each(function(field) {
var mapping = Ext.value(field.mapping, field.name, true),
rawValue = domQuery.selectValue(mapping, node, field.defaultValue),
value = field.convert(rawValue, node);

record.set(field.name, value);
});

return record;
},

/**
* Returns the total number of records indicated by the server response
* @param {XMLDocument} root The XML response root node
* @return {Number} total records
*/
getTotalRecords: function(root) {
var metaTotal = this.meta.totalRecords;

return metaTotal == undefined
? 0
: Ext.DomQuery.selectNumber(metaTotal, root, 0);
},

/**
* Returns true if the response document includes the expected success property
* @param {XMLDocument} root The XML document root node
* @return {Boolean} True if the XML response was successful
*/
wasSuccessful: function(root) {
var metaSuccess = this.meta.success;

//return true for any response except 'false'
if (metaSuccess == undefined) {
return true;
} else {
var successValue = Ext.DomQuery.selectValue(metaSuccess, root, true);
return successValue !== false && successValue !== 'false';
}
}
});

(For brevity I have omitted the existing readRecords comment blocks from the above)

I suggest that you structure your code in this way at least 99% of the time. The one exception is if high performance is an issue. If you are in a situation where every millisecond counts (you probably aren't), then taking the former route becomes more acceptable (though there's still no excuse for not adding a few comments explaining what the code actually does).

My refactored code almost certainly runs slower than the original as it doesn't take as much advantage of cached local variables as the monolithic version does. For library-level code this can make sense if the performance gain is significant, but for the everyday code you and I write it is rarely a good idea.

I'll be watching.

Continue reading