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
Acknowledging the Kroll cost
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?
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:
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.
The table structures in Titanium also provide bulk or individual modification APIs. Consider the following examples:
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.
When flushing the views within a hierarchy, you can either loop over the children array or call 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.
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.
The clearest take-aways 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!