November 15, 2019

Performance Optimizations in NativeScript

by Wern Ancheta

Performance Optimizations in NativeScript

Performance is very important to get right when it comes to mobile apps. Recent studies show that 53% of mobile users delete an app due to problems after just one attempt. Performance is especially important for cross-platform mobile app development platforms like React Native, Flutter, and NativeScript since they're not fully native and therefore can use some performance optimizations.

In this article, we're going to take a 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 - because 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. So the more complex your views are, the more it impacts the performance of your app. This will contribute to the overall sluggishness of your app.

Here's an example that uses custom components which are wrapped in a StackLayout to control which column of the GridLayout it 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 the 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 just directly pass the col or column attribute and you don't have to pass any attributes as an argument to your custom component:

<GridLayout columns="*,*" rows="*">
	<comps:my-sidebar col="0" />  
  <comps:my-content col="1" />
</GridLayout>

The approach we've gone through allows us to encapsulate the logic of customizing the layout within the component itself. In the snippet above, the main container decides what the col will be and that can no longer be changed - while the approach we've gone through 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

There are a few tips you can follow to optimize the loading of images in NativeScript. More often than not, you would 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 to the width of the device. Therefore, you already get this by default. Note that this feature is only available in Android. It will simply be ignored if used in 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 which are supposed to be displayed in the visible area of the screen. You can use the NativeScript Web Image Cache library to easily implement this type of functionality.

Protect your NativeScript App with Jscrambler

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 in 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 most likely have 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 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 it, 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 on 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 actual 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 up to the startup time. The official docs have covered how to add launch screens both 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's 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've used Facebook, Slack, or other popular apps, you've most likely encountered this UI pattern already. Basically, it just shows the expected structure that the actual content will be in to provide an 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. Here's a good tutorial on how to do it: NativeScript ListView Skeleton Screens with Angular & RxJS.

Conclusion

That's it! In this article, you learned some tips on how to optimize the performance of your NativeScript app. The main takeaway for this article is that you should always think of the performance impact of your coding practices before trying to implement it.

Reducing the view complexity, optimizing your images beforehand, taking advantage of view recycling, and using animations are all going to add up to make your NativeScript apps feel snappier.

For more performance tips, check out the following articles:

Before deploying your commercial NativeScript apps to production, make sure you are protecting their code against reverse-engineering, abuse, and tampering by following our tutorial.