Every now and then we receive feedback on the performance and APK size of some NativeScript Android application, especially built with Angular 2. In this post I will try to elaborate a bit more on the technical challenges behind the Android Runtime, Core Modules and Angular 2 integration projects as well as to share some easy steps that would boost the performance several times.
There are several tasks that happen while a NativeScript Android application loads:
Note: File extraction happens only upon the first application run after fresh installation. On next runs the files are read directly from the file system.
The first two steps are constant in term of execution time and are of no interest for optimization. Steps 3 and 4, however, are the main suspects for increased loading time. How do we optimize these? Meet the android-snapshot plugin.
What this snapshot plugin optimizes is:
The only trade-off is that, due to snapshot files being CPU-dependent, application package size is increased with additional 5MB. Still, we have plans how to further improve this (read more in the Package Size section).
If you take a look at the Readme file on the snapshot repo you will see the approximate numbers of our tests. With snapshot enabled a blank Anular 2 application’s loading time is improved nearly twice - 4 seconds vs. 2 seconds.!
V8 runs its own Garbage Collector and so does Android (Dalvik VM). Due to the architectural paradigms of NativeScript, especially the 100% native access through JavaScript one, native Android objects are sometimes proxied in JavaScript. Because of this, the Android Runtime uses complex mechanism to keep proxied native instances alive until the JavaScript proxy object gets collected. The problem is that the two Garbage Collectors live in separate worlds and run at their own pace. The Android Runtime tries to put equal memory pressure when allocating large native object - like Image - on the JavaScript side but this is just a hint. Sometimes the Dalvik GC needs to clean some large objects - like Bitmaps - to free heap memory but because these objects are proxied in JavaScript and proxies are still alive there nothing happens and the Dalvik heap is not cleaned. Most of the time the JS proxies can be collected by that time but V8’s GC hasn’t started yet, while the Dalvik one is already running. This eventually leads to a memory leak and OutOfMemory Android exceptions, especially when working with large native objects like Bitmaps and/or streams. A good read and example that covers this topic in deeper technical details may be found here.
With the above said - sometimes we need a mechanism to synchronize the two Garbage Collectors to ensure proper memory reclaim. V8 exposes a `gc()` call behind a flag and this flag is enabled by default for a NativeScript Android application. The NativeScript core modules take advantage of this behavior and call V8’s `gc` upon navigation. Why upon navigation? Because typically after a navigation action the previous page’s JavaScript Visual Tree becomes reachable by V8’s Garbage Collector. As I already mentioned collecting the proxies will make the corresponding native object reachable by Dalvik’s Garbage Collector.
All of this sounds like a perfect solution but unfortunately it leads to some performance issues when V8’s GC is unconditionally forced because it is a blocking operation running on the main UI thread. We are trying to call V8’s gc only upon main application loop idle but it seems this heuristic is not working as expected during navigation.
Calling JavaScript GC explicitly during navigation is one of the major performance bottlenecks within the NativeScript core modules. If you experience this today you may simply comment this line and see how it goes. We are currently working on a more generic solution that will be handled within the Android Runtime directly. Although it also uses some heuristics, it looks promising and seems to work in some 99% of the memory leak scenarios we’ve isolated so far.
As I explained in this blog post some time ago, the NativeScript Android Runtime ships with three separate builds for the three major CPU architectures available nowadays. This makes a blank Hello World application some 12MB big. If APK size is something that is critical for your clients then you may produce separate APKs for each CPU architecture your application needs to run on. For more information you may refer to the ABI splits section in the Publishing for Android help article.
Here is what you can do to improve the performance of your NativeScript Android application:
We have plans to further improve all of the above three major aspects of the overall performance in a NativeScript Android application:
Although NativeScript applications are 100% native and run fast and fluid in most of the cases, sometimes an application may need additional fine tuning, to become even faster. The NativeScript engineering team is actively working towards making all the above mentioned improvements enabled by default. As always, feedback is most welcome and much appreciated - please share it within the GitHub NativeScript and Android Runtime repositories.