Performance Optimizations in NativeScript
November 15th, 2019 | By Wern Ancheta | 7 min read
Performance optimizations aim to boost one of the most vital features of mobile apps: Performance.
Recent studies show that 53% of mobile users delete an app due to problems after just one attempt.
Performance is especially relevant for cross-platform mobile app development platforms like React Native, Flutter, and NativeScript since they're not fully native and can use some performance optimizations.
In this article, we will look at how we can optimize the performance of NativeScript apps. Specifically, we're going to tackle the following:
Reducing view complexity
Optimizing image loads
Optimizing lists
Using content placeholders and animations
Reducing View Complexity
The first thing you need to look at when optimizing for performance is the complexity of your views. Yes, there are many layout containers that you can choose from, and they make your life easy. This is where the problem comes in. The more layout containers you combine and use, the longer it takes to render your app.
The UI and interactions (gestures) all run on the same thread. The more complex your views are, the more they impact app performance. This will contribute to the overall sluggishness of your app.
Here's an example that uses custom components that are wrapped in a StackLayout to control which column of the GridLayout they will be rendered in:
<GridLayout columns="*,*" rows="*">
<StackLayout row="0" col="0">
<comps:my-sidebar />
</StackLayout>
<StackLayout row="0" col="1">
<comps:my-content />
</StackLayout>
</GridLayout>
If your my-sidebar or my-content components look like this, then you've already introduced a view that's at least three levels deep:
<StackLayout>
<!-- some content -->
</StackLayout>
As a solution, you can have your component accept the value for col as an argument from the main container. This way, you only use a single StackLayout instead of two:
<GridLayout columns="*,*" rows="*">
<comps:my-sidebar columnNumber="0" />
<comps:my-content columnNumber="1" />
</GridLayout>
Then bind the value to your view:
exports.onLoad = args => {
const container = args.object;
container.bindingContext = {
columnNumber: container.columnNumber
};
};
Lastly, use it in your component markup:
<StackLayout loaded="onLoad" col="{{ columnNumber }}">
<!-- some content -->
</StackLayout>
Note that you can directly pass the col or column attribute. You don't have to pass any attributes as arguments to your custom component.
<GridLayout columns="*,*" rows="*">
<comps:my-sidebar col="0" />
<comps:my-content col="1" />
</GridLayout>
Our approach allows us to encapsulate the logic of customizing the layout within the component itself. In the snippet above, the principal container decides what the col will be. That can no longer be changed, while our approach allows for some customization within the component.
The one we used above is just a simple example, but the main idea is to simplify your views as much as possible. This can be summed up with the following tips:
Simplify your views. Collaborate with the designers so you can simplify the layouts. More importantly, make them understand the performance cost of complex views so they can create designs with performance in mind.
Avoid using multiple layout containers as much as possible. Think of ways to implement a specific layout without using multiple layout containers. A good alternative for layout containers is using ng-template.
Leverage the use of GridLayout. As most layouts can be expressed using GridLayout anyway, it's best to try using it first before using StackLayout or FlexboxLayout.
Optimizing Image Loads
Follow some tips to optimize the loading of images in NativeScript. More often than not, you will most likely be loading images inside a list.
Therefore, your images must have been optimized on your server beforehand. This means running it through image optimization software such as ImageMagick before serving it. Users won't be able to use high-resolution images on mobile anyway, so it's better to serve a resized image. Once that's done, you can start implementing the following tips:
For large images, use decodeWidth and decodeHeight to downsample images so they take less memory. When these values are not set, they will be decoded according to the device width. You already get this by default.
Note that this feature is only available on Android. It will be ignored if used on iOS.
<Image src="{{ image }}" decodeWidth=”300” decodeHeight=”250”></Image>
Set the loadMode property of the image to async. This will allow the image to be loaded even if there are decoding and preloading operations.
<Image src="{{ image }}" loadMode="async"></Image>
Set the useCache property to true. This will use the internal memory and disk cache to store the image locally. This way, the images won't get loaded from the server again if they already exist in the cache.
<Image src="{{ image }}" useCache="true"></Image>
Here's a sample code bringing all the tips together:
<ListView id="listView1" items="{{ items }}">
<ListView.itemTemplate>
<Image src="{{ image }}" decodeWidth=”300” decodeHeight=”250” loadMode="async" useCache="true" stretch="aspectFill"></Image>
</ListView.itemTemplate>
</ListView>
Bonus tip: lazy load images so that the app only loads the images that are supposed to be displayed in the visible area of the screen.
You can use the NativeScript Web Image Cache library to implement these functionality types.
Optimizing Lists
Under the hood, NativeScript uses the native list controls to render ListView. By default, it will use UI Virtualization and View Recycling, so you already get a couple of performance benefits by default.
UI Virtualization means that it will only create views for items that are currently visible on the user's device. This allows it to consume less memory.
On the other hand, View Recycling means that when a view that's currently visible on the screen gets out of the viewport, it will be put in a pool of recycled items so that it can be reused when a new item becomes visible in the list.
If you're new to using ListView, it's easy to write bad code that will practically disable the performance boost you get from View Recycling.
If you're coming from Angular, you've most likely used ngIf in your lists to render a different template based on the value of a specific property for each item on your list:
<ListView [items]="items">
<ng-template let-item="item">
<StackLayout>
<GridLayout *ngIf="item.content_type === 'news'">
<!-- news content -->
</GridLayout>
<GridLayout *ngIf="item.content_type === 'video'">
<!-- video content -->
</GridLayout>
<GridLayout *ngIf="item.content_type === 'photo_album'">
<!-- photo album content -->
</GridLayout>
</StackLayout>
</ng-template>
</ListView>
The code above throws away any performance gains that View Recycling gives you by default. This is because it will now have to re-create all the views every time instead of recycling them, as it now has to decide which view to use.
If the current item uses a template that’s different from the next one, then it will simply be destroyed instead of being reused.
Here's what you want to do instead:
<ListView [items]="items" [itemTemplateSelector]="templateSelector">
<ng-template nsTemplateKey="news" let-item="item">
<!-- news content -->
</ng-template>
<ng-template nsTemplateKey="video" let-item="item">
<!-- video content -->
</ng-template>
<ng-template nsTemplateKey="photo_album" let-item="item">
<!-- photo album content -->
</ng-template>
</ListView>
The above code uses the item template selector to decide which template to use based on criteria you specify in the templateSelector() function:
public templateSelector(item, index, items) {
if (item.content_type === "news") {
}
// rest of the templates...
}
This helps you avoid using ngIf, which breaks View Recycling. By using template selectors, you get the same functionality as ngIf but with the benefits of View Recycling.
Using Content Placeholders and animations
Sometimes it's not just the performance that matters. Perceived performance matters as well. Making your users believe that your app is fast is part of the “game” as well. This is where content placeholders and animations come in. Here are a few tips:
Add a launch screen.
This is crucial to have in every app, especially those that have a lot of local assets that need to be loaded, as this adds to the startup time.
The official docs have covered how to add launch screens for Android and iOS.Use animated loading indicators.
When loading data from the server, show a loading indicator to inform the user that the app is loading something. Here is a good loading indicator plugin: NativeScript loading indicator.
You can also use NativeScript-Lottie to make fun animations that will engage the user while your app is processing something.Use content placeholders.
This is an alternative to using loading indicators. If you have used Facebook, Slack, or other popular apps, you have already encountered this UI pattern.
It just shows the expected structure that the actual content will be in to provide the illusion that the content is halfway loaded already.
You can use the template selector technique you learned earlier to show a "skeleton" template while the data is still being loaded from the server and then switch back to the real template once the data becomes available.
Read this tutorial on how to do it: NativeScript ListView Skeleton Screens with Angular and RxJS.
Jscrambler
The leader in client-side Web security. With Jscrambler, JavaScript applications become self-defensive and capable of detecting and blocking client-side attacks like Magecart.
View All ArticlesMust read next
Getting Started with Animations in NativeScript
Animations are a key component of a great user experience in mobile apps. With this tutorial, you'll learn how to create animations in NativeScript.
April 11, 2019 | By Wern Ancheta | 10 min read
MongoDB Native Driver vs Mongoose: Performance Benchmarks
Mongoose brings several useful features in Node, but how does it perform compared to the MongoDB native driver? Let's benchmark both using Express!
December 17, 2020 | By Camilo Reyes | 5 min read