Improving CSS performance of Cordova apps on Android TVs
I have recently been working in a team that creates web apps that sits inside a webview wrapper application on Android TVs. Coming from more of a web background, even though this is still using React and other common web technologies, there are still some interesting challenges.
If you have read the title of this blog, then you know we'll be talking about CSS performances on Android TVs. This is not a new topic, since performance is an issue on all low end spec devices, but the ones comes with TV are a bit different.
App architecture & Webview
To understand the problem, let's first take a look at how everything what we are dealing with. Essencially, there is a shell app which was created using Cordova which loads the web application into the view. Cordova, if you are new to it, connects up JavaScript events to native Android events and sensors. This means with Cordova it is now possible to detect things like "if the device has internet connection and etc".
We are not too concerned with the Cordova shell app, but it is worth knowing it uses whatever version of Webview that comes with the OS. Different versions of Android has different version of Webview, which makes sense since Android will just include the latest Webview when a new version of Android is released.
Since Android 4.4 (KitKat), the WebView component is based on the Chromium open source project. WebViews now include an updated version of the V8 JavaScript engine and support for modern web standards previously missing in old WebViews. New Webviews also share the same rendering engine as Chrome for Android, so rendering should be much more consistent between the WebView and Chrome. In Android 5.0 (Lollipop), the WebView has moved to an APK so it can be updated seperately to the Android platform. To see what version of Chrome is currently used on a Lollipop device, simply go to Settings < Apps < Android System WebView and look at the version. -- Chrome Team
Based on the above description by the Chrome team, this means the user could actually update the version of Webview. But in reality it is much safer to build for the default version of Webview since not many users won't be updating to newer versions. Luckily, all the TVs my team support are Android 5.0 (Lollipop) or later.
So why am I talking about Webviews, isn't this a blog on CSS performance? To understand the root problem and the solution, it is important to understand how Webview fits into all this. Next we'll be talking about the actual issue and solutions, and hopefully this will make more sense by then.
What is the problem?
I was working on a new feature which allowed user to browse and find the right content by using a grid navigation. The grid will scroll vertically depending on if the user wishes to see more content (no horizontal scroll). When the user tries to scroll to a row that is outside of the current viewport, there is a nice shifting transition that brings all the tiles upwards or downwards. Then the focus maintains on the new tile and the new row will be within the viewport.
After the implementation has been done, the feature was tested on one of the highest spec TVs and I noticed some weird behaviors.
- Verticle navigation is way more jumpy and laggier than horizontal navigation
- This issue seems to be only occuring on the TVs, on our development machines (Mac) everything works fine
It didn't take long for me to conclude that this is a performance issue.
The solutions
After some research and investigation using the Chrome browser tools, I couldn't find anything suspicious. However, I came across some discussions around how some people have tried to trigger GPU rendering instead of CPU for better CSS animation performance.
There are two ways of achieving this:
- TranslateZ(0) hack: It is normally used in CSS to translate a component on the Z-axies as the name suggests. A less well-known functionality is that even by declaring 0 translate (no change) it will force hardware acceleration and thus render with the GPU instead of CPU.
- will-change: prepares the browser to complete optimizations before an element is actually changed. As a result, the transition will appear much smoother to the end user even on low end devices.
After testing both of the ideas above, everything looked the way I expected. So which of the two solutions should we pick? The normal thinking might be that since will-change
is designed for this and TranslateZ()
is a hack, it only makes sense to use will-change
right?
Well, I actually went with TranslateZ()
due to the following reasons:
- Using
will-change
too many times in the code will actually cause enough overhead to decrease the performance of the web app. The recommended approach is to addwill-change
on the element right before the animation, then take it off the DOM again. Which requires a lot of additional logic in place to add and remove it from the elements, which in this case there quite a few. - The other reason being
will-change
was released not too long ago, so the support is a bit lacking in older browser version. If there happens to be a TV with older Webview then this optimisation apply.
It is worth to point out that over using TranslateZ()
would also lead to performance issues. Each time it is used, it will move the applied component to its own composited layer. So even if you do decide to use this hack, try to keep it at a minimum. Overall, based on my research I felt that this would be less of an issue compared to will-change
.
To learn more about will-change
checkout css will-change property by Opera and re-rastering composite layer by Google.
BUT WAIT! What about the fact this issue only occured on TVs and not on the development machines? My conclusion is that newer version of Chrome/Chromium must be doing some kind of optimisation by default for 2D translates, because when I checked the Mac's Chrome dev tools (without the above fix) the GPU was already being used to do the rendering for those transitions. This explains why I never saw the issue on Mac and the TVs had this issue because they were on lower version of Webview that didn't have this optmisation built-in.
Final Thoughts
The TranslateZ()
hack is a good way of improving CSS performance for sites with a lot of transitions. However, since hardware acceleration is forced and GPU render drains more battery, one thing to bear in mind is that we may want to avoid doing this on mobile devices or anything that runes on battery (e.g. unplugged laptops). For example, in the future we could use navigator.getBattery() and user-agent to detect the user's device and bettery state. We may then wish to disable transitions to best optimise for mobile browsing experience. Obviously, this is not a concern for plugged in devices and TVs which are plugged in all the time.
There are a few other things I'd like to mention:
- I joined this project late after everything was already setup. So I am not sure why React-native or other ways of developing for cross-platform wasn't picked instead
- Not all smart TVs are based off Android, so the performance issue in some TVs could be a lot worse than what I explained (and may not even apply). On the other hand, not all systems demand as much resources as Android does. So your mileage may vary.
- Amazon's FireTV stick is also based on Android, and with the FireTV 4k's recent release, the market share of Android TVs is very likely to further increase.