As cross-platform developers, we all know that maintaining speed in a complex codebase is of paramount importance. When you’re adding layers of abstraction to your code in hopes of being able to share large portions of it across disparate platforms, the little steps you have to take to synchronize your common code with the underlying platform-specific code can quickly add up to a massive slowdown that leaves you with an application that performs and feels no better than a mobile web application plopped into a WebView.

The Kroll Bridge

Titanium effectively acts as a three-tier framework. At the lowest level, there is a layer of native code used to implement core application functionality. These are your views, your HTTP clients, and your other pieces of code that reach out and allow you to interact with device features like the camera and geolocation service. On top of these platform-specific native layers lies a platform-specific bridging layer, which allows you to abstract away the complexity of these native objects and translate them into Javascript-land objects (called proxy objects). Finally, on top of this bridging layer lies your application-specific Javascript code, which is where the bulk of your application logic will reside.

Titanium’s abstraction and the bridging layer is known as the Kroll Bridge, and it represents the single biggest bottleneck in a Titanium application. The Kroll bridge is exactly what it sounds like, it’s a connection between the native objects in your application provided by the Titanium SDK and the Javascript proxy objects that stand in their place within your Javascript code. Every time you update one of your proxy objects, the Kroll bridge gets to work synchronizing your changes to the native object. Because of this automatic synchronization logic, it’s easy to make a huge number of Kroll calls without really recognizing what you’re doing, which leaves you with an application that has no obvious performance issues (from a code perspective) that has distinct slowdowns when placed on a physical device.

Acknowledging the Kroll cost

Titanium provides various methods of sending data back and forward between your Javascript code and your native code. For the most part, these can be divided into the categories of bulk operations vs sequential operations. Going into this blog post, I had intended to write at length about the virtues of batching your calls and minimizing your Kroll crossings, however, it seems like there may be more nuances to the performance concerns in Titanium than I had thought! For the purposes of this blog post, I’ve taken a look at how long various operations block the Titanium thread.

I’ve prepared a couple of minimal test cases, with built-in timestamping and simple profiling code, so feel free to test these cases on your own devices! For each of these tests, we’ll run some operation 1000 times and take the difference in milliseconds between the start of the operations and the end. Note that this testbed only takes a look at how long different operations tie up the Titanium thread. Many of these tests tie up the UI thread for a substantial amount of time as well and will cause the app to become unresponsive. The UI thread performance of these tests are outside of the scope of this blog post, I’d like to circle back in a few weeks and take a closer look at the UI impact of these tests as well.

I ran all of my tests on a Nexus 5 running Android 6.0 Marshmallow and an iPhone 5 running iOS 9.1. I ran six trials of each of these tests and averaged the results. I’ve published the test code on GitHub. Take a look, give it a clone, and follow along on your own devices.

Creation arguments vs Create then set

Titanium provides factory methods to create proxy objects, which allow you to interact with native objects that you may need multiple instances of. Optionally, these methods accept a creation dictionary describing its initial state. Of course, you can just make the proxy and then configure it later, what’s the difference?

var newView = Ti.UI.createView();
 
newView.top = 5;
newView.height = 40;
newView.width = Ti.UI.FILL;
newView.left = 5;
newView.right = 5;
newView.backgroundColor = 'red';
var newView = Ti.UI.createView({
    top : 5,
    height : 40,
    width : Ti.UI.FILL,
    left : 5,
    right : 5,
    backgroundColor : 'red'
});

On iOS, this behaves largely as expected. Creation with arguments returns a little faster than the creation followed by sets. On Android, however, in addition to being substantially slower, the creation dictionary actually slowed the creation process down!

Sequential updates vs applyProperties

Similarly, Titanium provides both individual property set APIs as well as a bulk application API. Take the following examples:

view.height          = 80;
view.backgroundColor = 'blue';
view.left            = 10;
view.right           = 10;
view.applyProperties({
	height          : 80,
	backgroundColor : 'blue',
	left            : 10,
	right           : 10
});

Oddly enough, we observe the opposite behavior here from view creation! Android performs as expected, with the bulk API yielding better performance. iOS, on the other hand, runs quite slowly, and the bulk API is slower than the sequential sets.

TableView population

The table structures in Titanium also provide bulk or individual modification APIs. Consider the following examples:

for(var ii = 0; ii < 1000; ++ii){
	var newRow = Ti.UI.createTableViewRow({
		top : 5,
		height : 40,
		width : Ti.UI.FILL,
		left : 5,
		right : 5,
		backgroundColor : 'red'
	});

	theTable.appendRow(newRow);
}
var tableData = [];
for(var ii = 0; ii < 1000; ++ii){
	var newRow = Ti.UI.createTableViewRow({
		top : 5,
		height : 40,
		width : Ti.UI.FILL,
		left : 5,
		right : 5,
		backgroundColor : 'red'
	});

	tableData.push(newRow);
}

theTable.setData(tableData);

Finally, an expected result! The bulk population API is massively more performant than the individual population API. Android is still a little slower than iOS, but that is mostly expected.

View Removal

When flushing the views within a hierarchy, you can either loop over the children array or call removeAllChildren.

var children = theWindow.children;

for(var ii = 0, numChildren = children.length; ii < numChildren; ++ii){
	theWindow.remove(children[ii]);
}
theWindow.removeAllChildren();

Another API that performs differently on iOS vs Android. On iOS, the call to removeAllChildren is almost immediate, whereas on Android the call takes even longer than looping over the entire child list and removing them individually.

Event Firing

Titanium exposes a built-in eventing API used for communicating between native code and Javascript code. Additionally, Backbone exposes an eventing API for communication between Javascript objects. I frequently see the Titanium eventing API repurposed for use in Javascript-land communication, let’s see what the impact is.

var startTime;

var handledCount = 0;

function testListener(){
	handledCount++;

	if(handledCount === 10000){
		var endTime = new Date();
		var delta = endTime - startTime;

		alert('fired 10000 Ti.APP events in ' + delta + 'ms');

		Ti.App.removeEventListener('testEvent', testListener);
	}
}

Ti.App.addEventListener('testEvent', testListener);

startTime = new Date();

for(var ii = 0; ii < 10000; ++ii){
	Ti.App.fireEvent('testEvent');
}
var startTime;

//since events fire asynchronously, we need to keep track of how many were handled.
var handledCount = 0;
var eventingObj = _.extend({}, Backbone.Events);

eventingObj.on('testEvent', function(){
	handledCount++;

	if(handledCount === 10000){
		var endTime = new Date();
		var delta = endTime - startTime;

		alert('fired 10000 Backbone events in ' + delta + 'ms');
	}
});

startTime = new Date();

for(var ii = 0; ii < 10000; ++ii){
	eventingObj.trigger('testEvent');
}

Another substantial difference. Backbone events perform consistently (and impressively well!) on both platforms, whereas Titanium events are much slower on iOS, and are a little slower on Android.

Takeaways

The clearest takeaways from these tests are that one needs to be much more careful while modifying the view hierarchy on android, that Ti.App events are quite slow, and that there isn’t a one-size-fits-all performance solution for Titanium. There’s no magic bullet that you can adopt in your codebase and not have to worry about platform-specific performance issues. Android’s slowness when handing creation dictionaries and iOS’s aversion to applyProperties makes it more difficult to write platform-agnostic performant code. That being said, in the general case, applyProperties is still worth using, because of the small performance hit we took on iOS and the performance bump we get on Android (which is usually the issue from a performance perspective).

At the end of the day, there’s no substitute for profiling your application and making use of the platform-specific Alloy preprocessor directives (OS_ANDROID, OS_IOS) to deal with platform-specific performance issues. And now that we’re armed with concrete data, we’re ever so slightly better equipped to do that!

Sign up for the Shockoe newsletter and we’ll keep you updated with the latest blogs, podcasts, and events focused on emerging mobile trends.