I think some of the most common questions people ask when developing PhoneGap apps are about performance. How do I make my app faster? How do I make it feel native? Any tips of making an app feel at-home on a particular platform? How do you match native OS “look and feel”? In this post, I’ll attempt to provide some insight on techniques for creating great PhoneGap apps, and also try to debunk a few myths floating around the “interwebs”…
I’ve already posted about workflow and app store approval processes, and in this post the primary focus is runtime performance.
When we are talking about performance, we are really talking about perceived performance and responsiveness from an end-user perspective. Since PhoneGap applications are based upon HTML & web views, you are limited by the performance of a web view on a particular platform. With that said, that doesn’t mean all HTML-based apps are inherently slow. There are very many successful apps with an HTML-based UI. Some even get featured in the app stores. We can’t make the web views faster, but we can help you make your HTML experience faster inside of that web view.
However if the web view isn’t fast enough for your taste, don’t forget that you can use PhoneGap as a subview of a native app. In this case, you can leverage native user interface elements when you need them, and leverage PhoneGap’s HTML/JS user interface when you need it. Then use PhoneGap/Cordova’s native-to-JS bridge to handle bidirectional communication between the native code and the JavaScript inside of the web view component. More on this later…
HTML/Web View Performance
First, let’s talk specifics about mobile HTML & web performance. There are a lot of common “tips” floating around, some of these are extremely valid, although others are misleading. Please allow me to clear some things up…
Hardware Acceleration
All over the the web, you can see generic statements telling you to force hardware acceleration whenever possible, in every scenario, just by adding a css translate3d transform style:
[js]transform: translate3d(0,0,0);[/js]
While it is true that you can force GPU rendering of HTML DOM content, you really need to understand how it can impact the performance of your application. In some cases, this can greatly improve the performance of your app, while in other cases, it can create performance issues that are extremely difficult to track down. In other words: Do not use this for every case, and on every HTML DOM element.
You should be very selective when you use transtale3d to force hardware acceleration for a number of reasons.
First, whenever you are using translate3d, the content being accelerated takes up memory on the GPU. Over-use of GPU rendering on HTML DOM elements can cause your application to use up all of the available memory on the GPU. When this happens, your app could either crash completely without any error messages or warnings, or the GPU could swap memory content from device RAM or permanent storage. In either of these cases, you will have worse performance than you did without forced GPU rendering.
Second, whenever you apply a translate3d transform, this causes the content of the HTML DOM element to be uploaded on the GPU for rendering. In many cases, the GPU upload time is imperceptible, and the results are positive. However, if you apply the translate3d style to a very large and very complex HTML DOM element, a noticeable delay can be encountered, even on the latest and greatest devices and operating systems. The more complex the HTML DOM, the larger the delay. When this happens, the web view (or browser) UI thread can be completely locked up for that period. While the worst I’ve seen of this was less than a second, it still can have a profoundly negative impact when your entire UI locks up for 500ms. In addition, any time you make changes to a DOM element (content or style-wise), that texture has to be re-uploaded to the GPU.
Third, avoid nesting DOM elements that use translate3d. Let’s consider a case where you have lots of DOM elements inside of a <div> elements, multiple levels deep, and all of those elements, including the outtermost <div> use translate3d for GPU rendering. For the outter-most <div> to be rendered, all of it’s child elements have to be rendered. In order for all of those child elements to be rendered, all of their child elements have to be rendered, etc… This means that all of the child elements have to be uploaded to the GPU before the parent element can be rendered. This adds up very quickly. Nested elements that use the css translate3d GPU hack will take longer to load, and can take up significantly more memory, causing very poor performance, slow GPU upload times, or complete app crashes (see paragraphs above).
Fourth, consider the maximum texture size for the GPU. If the DOM element is larger than the maximum texture size supported by the GPU (in either width or height), then you will encounter very poor performance and visual artifacts. Have you ever seen HTML DOM elements flicker during an animation or when scrolling, when using translate3d? If so, then there’s a good chance that the DOM element’s dimensions are larger than the texture size supported by the device’s GPU. The maximum texture size on many mobile devices is 1024×1024, but varies by platform. The maximum texture size on an iPad 2 is 2048×2048, where the maximum texture on the iPad 3/4 is 4096×4096. If the device maximum texture size is 1024, and you DOM element’s height is 1025 pixels, you will have those pesky flickering and performance issues.
Note: In some platforms, you can also have flickering due to the browser implementation, which can often be corrected by setting backface-visibility css. See details here.
Fifth, remember that you are on a mobile device. Mobile devices tend to have slower CPUs, slower bus, and less memory than desktop devices. While you may get great results leveraging translate3d on the desktop, it could kill your experience on mobile. In addition, different platforms have different levels of support for hardware acceleration, and performance can vary.
Translate3d can definitely yield positive results, but can also yield negative results too, depending upon how it is used. Use it wisely, and always, always, always test on a device.
The Impact of Content Reflow
If you’ve never heard of “reflow” before, you should pay attention. “Reflow” is the browser process for calculating positions and geometries for HTML DOM elements. This includes measurement of width & height, the wrapping of text, the flow/relative position of DOM elements, and basically everything related.
The calculations used by the reflow processes can be computationally expensive, and you want to minimize this cost and the amount of content reflow to achieve optimal performance. If you’ve ever animated the width of a HTML DOM element, and seen the framerate drop to to 5 FPS or lower, then you’ve seen the impact of reflow processes in action.
Reflow processes are invoked any time DOM content changes, DOM elements are resized, CSS positioning/padding/margins are changed, etc… While you may not notice any impact of reflow processes on desktop browsers, reflow can have a significant performance impact on mobile devices. You can never get rid of reflow processes altogether (well, unless your content absolutely never changes), but you can mitigate the impact of those relflow processes.
If you’d like to learn more about reflow processes, I strongly suggest reading these:
If you’ve seen lists of tips for PhoneGap & Mobile web apps, you may have seen statements like “minimize DOM nodes”, “avoid deeply nested html”, use css animations, preload images, etc… The reason that these are suggested is that they are tips for minimizing the impact and occurrence of reflow operations.
- Minimize DOM node instances – The fewer DOM nodes, the less amount of nodes that have to be measured and calculated in reflow operations.
- Avoid deeply nested HTML DOM structures – The deeper the HTML structure, the more complex and more expensive reflow operations. In addition, changes at the lowest child level will cause reflow operations all the way up the tree, ending up more computationally expensive.
- Use CSS transforms – In addition to forcing hardware acceleration as discussed above, you can alter HTML DOM elements *without* invoking reflow processes if you change them using CSS3 transform styles. This could be translation on the X, Y, or Z axis, scaling, or rotation. However, be careful not to overuse css translate3d for the reasons mentioned above.
- Use CSS animations & transitions – CSS animations and transitions can definitely make things faster. However this is not a “catch-all” that covers every case. If you use CSS transitions with properties that introduce reflow operations (such as width or height changes), then your performance could stutter. You can achieve fluid animations by leveraging CSS transitions, in conjunction with css transforms (as mentioned above) b/c they alter the visual appearance, but don’t invoke reflow operations.
- Use fixed widths and heights for DOM elements – If you don’t change the size of content, you don’t invoke reflow operations. This could be for <div> (or other) elements, or it could apply to the loading of images. If you don’t predefine an image size, and wait until the image is loaded, then there is an expensive reflow operation when the image loads. If you have multiple images, this adds up.
- Preload images or assets used in CSS styles – This is a benefit for two reasons. First, your images will already be ready when you need them. This prevents a delay or flicker when waiting for content. Second, if you already have images or other assets preloaded into memory before they are needed, then you don’t have a secondary reflow operation that could occur after the image/assets have been loaded (1st reflow is when DOM is initially calculated, 2nd when its recalculated on load).
- Be smart with your HTML DOM elements – Let’s say you want to create and populate a <table> element based on data you have in a JavaScript array. It is very expensive to first append a <table> element, then loop over the array and individually append rows to the existing DOM on every loop iteration. Rather, you could loop over the array, and create the HTML DOM element for the table only. Then, after the loop has been completed, append the <table> to the existing HTML DOM. Every time that you append DOM elements, you invoke a reflow operation. You want to minimize these operations.
In general, you want to minimize the amount of work required by the browser to calculate layout and positioning of DOM elements.
Keep Graphics Simple
Designers love to make great looking mockups, but sometimes the implementation of such designs don’t always yield great performance. Overuse of CSS shadows and CSS gradients can impair runtime performance across different platforms & browser implementations. I’m not saying don’t use them at all, just use them wisely. For example, CSS gradients and shadows induce slower performance on Android devices than a compariable iOS device, based on the OS/system web view implementation.
Touch Interactivity
You’ve probably heard before that “mouse events are slow and touch events are fast”. This is absolutely a true statement. Instead of using “mousedown”, “mousemove”, “mouseup”, or “click” events, you should use touch events: “touchstart”, “touchmove” and “touchend”.
On mobile devices, mouse events have latency that is introduced by the operating system layer. The OS tries to detect if a gesture has occurred. If no gesture has occurred, it passes the event on to the web view as a mouse event. When you use touch events instead of mouse events, you recieve the event immediately, without the operating system-introduced latency.
You may have noticed that there is no equivalent of “click” for touch events… That’s right, there isn’t one. However, you can manually manage the touch events yourself to infer a click action, or you can leverage an existing library to infer “taps” or gestures. Here are a few for consideration: Zepto, FastClick, Hammer.js, iScroll (yes, iScroll removes the click delay for you inside the scrollable area)
JavaScript Optimizations
In essence, write efficient code that doesn’t block the UI thread. I could go on for a while on this, but instead, just follow language optimizations and best practices. Here are a few articles on the subject:
There are lots of facets and permutations to consider when building your apps, so just try to write decent code that you wouldn’t be embarrassed to show to your coder friends.
Native Performance
So, let’s say that you want a native user experience, native navigation between containers, but you also want the ease of content creation with HTML? Nobody said you can’t use PhoneGap as a subview of a native application. In fact, there are some VERY widely used apps in existence that employ this technique… If only I could tell you (NDA).
This approach requires native coding expertise, but you can use CordovaView as a subview inside of a native application on both iOS and Android platforms. This allows you to take advantage of native components when you want to, and leverage a PhoneGap/HTML UI when you want to. PhoneGap provides you with the native-to-JavaScript bridge for full bidirectional communication between the native and JavaScript layers.
When using HTML, it is really easy to create custom UIs, whether they are complex graphics, tabular data, or anything really. With the CordovaView approach, you let each layer (native/HTML) do what it does best.
You can learn more about embedding the CordovaView inside of a native application from the PhoneGap Docs.
UI & UX Considerations
At risk of sounding overly-generalized: You want to pay attention to building a quality user experience. If you don’t focus on quality, your application will suffer. I’ve already covered user interface and user experience considerations in detail, but I’ll highlight one common question that I encounter: “How do I make an app that exactly matches native look and feel for all platforms?”
My response: Don’t.
Let me clarify this point: Im not saying don’t build something that looks “at home” on a particular platform. I’m suggesting that you don’t try to match the operating system in every single detail. The main reasons being that 1) this is very difficult to do, and 2) if anything changes in the OS, it will be very obvious in your application.
You may or may not have heard of the “uncanny valley” effect. When applied to PhoneGap apps, if you get really, really close to behaving exactly like a native app, but there are minor differences, then it can immediately signal that “something is different” or “something is wrong” with your app, and can cause an undesired negative experience for the user.
Instead, I recommend creating a unique identity for your app (that still meets App Store guidelines). This includes look and feel, button styles, navigation, content, etc… Rather than mimicking every aspect of the native UX, build a cohesive experience around your identity or brand, and carry that identity through all of your mobile apps. People will associate this identity with your app, rather than comparing it to the native OS.
However, if you do want to mimic native look and feel, it can be done completely with CSS styles.
Test On Devices
I can’t emphasize this point enough. Always test on a device. Always. I like to target older devices, because if you can make it fast on an older device, it will be even faster on a newer one. Be sure to test on-device on all platforms that you are trying to target. For iOS, I test on an iPhone 4 (not 4s) and and iPad 2. On Android, I use a Motorola Atrix, Nexus 7 tablet, Kindle Fire (first generation), and a Samsung Galaxy 10.1 (first generation). I test other platforms whenever I borrow a device, and pretty much use whatever I can get my hands on. Heck, I’ve even gone into retail stores just to install an app on one of their display devices to see how it looks.