Mobile App Performance and Size Optimization

iOS vs Android

Smartface is supporting iOS and Android development. Both working with Native API access. For more information how this works please check Smartface Native Framework Architecture document. iOS and Android are very different platforms. They have different architectures. Smartface is bringing layers for single code base app development. Some differences are managed on the Native side, some of them are managed in the sf-core side. Even though Smartface is managing those differences, but still it is bound to the limitations of the platform which it is running on. For that reason, while developing apps, developers will notice performance differences between iOS and Android. Here is a comparison of some fundamental performance differences between iOS and Android:

Feature

iOS

Android

File operations

Faster

Slower

Image creation from file

Faster

Slower

Blob operations

Faster

Slower

Page Creation

Faster

Slower

Property assignment

Faster

Slower

The features mentioned above should be used with care. The developer needs to reduce the usages of those features excessively. This will not just benefit Android, iOS will benefit too.

File Operations

There are several calls that result in file operation:

General tips

  • Try to reduce the number of calls

  • Define them on top of the file as much as possible as long it suits

  • require uses caching by (absolute) paths. When a resolved path is a match, it reuses the cached module export. Resolving the path is a CPU operation if not a file (for cached results). Using require on top of the file at least reduces the CPU calls.

  • Define images on top of the file, as much as possible.

  • Cache values as much as possible. For Page instance-related values, using of WeakMap and WeakSet is advised.

  • Use global variables or modules to share data across the files, do not use the Data or Database modules. Data & Database to be used only with persistent data.

Blob

Android parses blob (Binary Large Object Block) slower than iOS. This resulting in converting binary data to other formats.

  • Try to cache the converted values (such as images)

  • Try to use small amounts of HTTP data while parsing responses. Make sure APIs responds only relevant data.

  • If an image needs to be downloaded from the internet and to be showed on an ImageView and caching is not required, using of ImageView.loadFromUrl is advised. If possible using async task will also speed-up the process.

Pages

Pages are the core of the application. They form whole screen UI and also used with navigator & SwipeView. Creation of a page has a cost. The cost is considerable for both iOS & Android. To reduce the number creation of pages, defining them as a singleton for Router and Navigator is important. As a side effect, this approach is making the state management of pages mandatory, such as the need to reset as they are shown in some cases. Anyway resetting the data & display costs definitely less than the creation of whole new UI page and children.

SwipeView

SwipeView pages are created partially when assigned. Setting the pages property causes recreating of all pages as it is assigned. If data on the SwipeView pages to be changed, but not the number of pages, then this SwipeView.pages property should not be re-assigned, instead data should be modified on the instances of the pages. How to access to the SwipeView instances is explained in SwipeView Guide.

UI Thread

JavaScript is a single-threaded environment. setTimeout and setInterval creates a timer call back to the same main JavaScript thread. In Smartface JavaScript thread is performing UI operations. In order to access UI objects from JS, JS main thread and the UI thread are the same. So everything that causes to wait on the JS side can cause hanging and freeze on the UI. In order to avoid such behavior, and perform partial UI updates, dividing the big tasks into smaller tasks by using timers (setTimeout, setInterval) is advised. There is no doubt that mobile device performance increases day by day. Still, mobile apps should be considered as thin clients of the system. The heavy burden of calculation should be performed on the servers as much as possible. Even such an architectural design needs some performance tuning: UI operations. Heavy cost UI operations on the app will make the app run slower. In order to fine-tune those operations following things can be done: ## Splash Splash is shown until the first page appears the screen, caused by Router.go. If there are some heavy operations that need to be completed, those operations can be triggered before showing the first page. This approach is extending the splash duration (app start) and reduces the UI freeze, it is a trade-off actually. ## BottomTabBar BottomTabBar is creating pages and uses navigator. This is causing creating bulk of pages when it is initiated, some of its pages are created on demand. This is considered a heavy UI operation. If needed, this operation can be moved to the app start. ## Setting data during onShow Page.onShow event is fired after the page is even partially shown. This event is giving enough time for developers to bind data. If the showing of the page is slow (due to the big constructor and onLoad operations) some of the tasks can be moved to onShow. This causing a faster appearance of the page, but still, the total loading time will be the same.

Using state management and subscribing to changes

When using a state management library like Redux or Flux to handle your own states, make sure that the subscribes you define on the page is unsubscribed on the page removal. You can use onHide for such cases, removing the risks of unexpected UI rendering on a page which doesn't exist anymore.

Async Task

Smartface introduces a new feature called AsyncTask. The task function is running on a separate thread, should not access (will result in a crash) to UI components and no UI operation should be performed on this task. It is useful to load some heavy libraries, which do not depend on the UI (such as google-libphonenumber). This async task is not fully compatible with any library. Some libraries might cause a problem depending on each System (iOS or Android) and might result in hang or crash. This is determined by trial and error. Util lib offers a promise based solution.

Property assignment

In Smartface, with sf-core assignments directly affecting the UI object.

Sample Assignment
Sample Assignment
//before
page.textBox1.text = "Smartface"; //assignment
//after

When the assignment occurs, it goes to the next line after UI operation is complete. This can cause an overhead if the original value before the assignment is the same with the value to be assigned after.

Same assignment
Same assignment
page.flexLayout.backgroundColor = Color.RED;
var myRed = Color.create("#FF0000");
page.flexLayout.backgroundColor = myRed;

The example above is triggering the UI rendering twice for the same outcome, wasting a precious time of execution. It will be better to assign a property if the value is different what it originally had. For that solution, Smartface Contx is very handy. Contx is a base style state management tool shipped with Smartface. Contx is keeping track of changes on the object, is called state. As long as all the changes are done via Contx, the state will not be broken. Before making an assignment with Contx, it is comparing the value to be assigned with the original values, only the different ones are assigned.

Contx Assignment
myFlexLayout Class
Contx Assignment
page.flexLayout.dispatch({
type: "pushClassNames",
classNames: ".myFlexLayout"
}); //this makes the background color as red
this.dispatch({
type: "updateUserStyle",
userStyle: {
backgroundColor: "#FF0000"
}
}); //original color is already red, no UI update
myFlexLayout Class
{
".myFlexLayout":
{
"backgroundColor": "#FF0000"
}
}

If the code is written out of the state, Contx will not be aware of the changes. Below is a bad example, and do not write such code as much as possible.

Bad State Management
myFlexLayout
Bad State Management
page.flexLayout.dispatch({
type: "pushClassNames",
classNames: ".myFlexLayout"
}); //this makes the background color as red
page.flexLayout.backgroundColor = Color.BLUE;
// Contx still thinks the background color is red
this.dispatch({
type: "updateUserStyle",
userStyle: {
backgroundColor: "#FF0000"
}
}); //original color supposed to be red, so Contx state does not see any change, so it does not perform a UI update
myFlexLayout
{
".myFlexLayout":
{
"backgroundColor": "#FF0000"
}
}

MapView

MapView is not a light view. In order to interact with the map, it is advised to use the onCreate Event. If a page has a MapView, postpone the following actions after the event is called:

  • Service call, which is going to populate the map pins

  • Add pins

  • Add map events (such as move)

  • Add additional views/controls to interact with the map to the page (or dialog)

Using this will have minimal impact on iOS, will have an impact on Android. As a UI guidance, showing an activity indicator is advised.

MapView does not keep a flag that it is created or not. Also, in some cases, onCreate might be triggered before Page.onShow event. Best practice to handle that event is to use a code like this:

MapView creation with Page.onShow
MapView creation with Page.onShow
function pageConstructor() {
setupMapReadiness(page);
}
function onShow() {
tickMapReady();
page.mapReady(()=> {
//perform the service call
});
}
functon onMapViewCreate() {
tickMapReady();
}
function buttonPress() {
page.mapReady(() => {
//do any action for intreacting the map
});
}
function setupMapReadiness(page) {
var tick = 0;
var readyList = [];
page.tickMapReady = function tickMapReady() {
tick++;
if(tick >= 2 && readyList.length > 0) {
readyList.forEach(fn => fn.call(page));
readyList.length = 0;
}
};
page.mapReady = function mapReady(fn) {
if(tick >= 2) { //shown & created
readyList.push(fn); //adds them
} else {
fn.call(page); //map is ready, call it as requested
}
}
}

BottomTabBar

BottomTabBar (btb as short) is managing pages like a router does. It is responsible for showing pages. Pages are created and shown as btb instructs. In the normal behavior of btb, pages are created on demand, as the user changes the tabs. According to that flow:

  1. The tab is about to change

  2. The target page is created, constructor called

  3. The page is loaded, onLoad event called

  4. Tab changed

  5. The page is shown, onShow event is called

Using heavy operations, too much controls & views may slow down the changing of the tab. A case looking fine on iOS might work too slow for Android. Performing the following actions might speed up the btb performance:

  • Creating pages as they are being added to btb

  • Adding UI components/views on-demand

  • Performing actions after onShow

  • Map related actions

Creating pages as they are being added to BottomTabBar

add method of the btb has a new flag for Android eagerLoading. Setting this truewill put the burden of the constructor time to the app start (assuming btb is created as the app is started). Be careful where to put this load, this is not reducing the time of the page, just arranges it.

Adding UI components/views on-demand

If the page components are to be shown after some asynchronous call, it is advised to not to add those components beforehand to the page. Create those views using library and add them to the page on demand. This will greatly improve the performance of tab switching.

Performing actions after onShow

Some pages containing lots of UI components/views, including static ones. Using those pages in btb might slow down the changing of the tab. It is advised to show an ActivityIndicator and add those components after the page is shown. (not changing the visibility; adding)

Using MapView in btb can have an impact on switching the tabs. If the guideline above is followed the impact will be minimum.

Life-cycle

If a page is shown once, only the page.onShow event is fired:

  1. The tab is about to change

  2. Tab changed

  3. The page is shown, onShow event is called

If the guidelines above are followed, make sure that the post-onShow UI operations may occur once only.

Lazy Page Loading via Router & Navigator

There are several ways to add a page to the router:

classic adding
classic adding
const Router = require('sf-core/router');
Router.add("page1", require("pages/page1"));

In the example above, page1 is added to the router by running the code in pages/page1.js. A block in the pages/page1.js might slow-down this operation. Also it possible to add a page to the router, requiring the source code of the page and all related sub-modules, which the page is never going to be shown during app is running. This might result in start-up slowness. In order to add those pages on-demand it is possible to add them by the path of the .js file.

Lazy loading of pages
Lazy loading of pages
const Router = require('sf-core/router');
Router.add("page1","pages/page1");

Using this string path requires giving of absolute path of the source file. If dynamic usage of a path is required __filename variable in the module can be used to resolve the relative path.

Avoid ScrollView

ScrollView, ListView, GridView can be used to show vertical scrolling content. In ScrollView all of the child components are created at the beginning while ListView and GridView are only creating the items which are visible only and they are creating rest of the items on-demand as user scrolls. It is advised to avoid ScrollView as much as possible if a ListView or GridView can be used even with non-uniform items. This approach will also increase the performance of BottomTabBar, because items can be rendered during onShow.

Caching

With JavaScript tricks, caching mechanisms can also help performance. We have a scenario to give an example of caching.

Without Caching
Without Caching
listView.onRowSelected = function(listViewItem, index) {
showMenu(data);
};
function showMenu(data) {
var menu = new Menu();
var menuItemCopy = new MenuItem({
title: "Copy"
});
menu.items = [menuItemCopy];
menu.headerTitle = data;
menuItemCopy.onSelected = function() {
alert(data);
};
menu.show(page);
}
With Caching
With Caching
listView.onRowSelected = function(listViewItem, index) {
showMenu(data);
};
var showMenu = (function() {
// This block runs only once and variables are cached
var menu = new Menu();
var menuItemCopy = new MenuItem({
title: "Copy"
});
menu.items = [menuItemCopy];
// End of block
// Main logic, this block runs every time when showMenu is called
return function(data) {
menu.headerTitle = data;
menuItemCopy.onSelected = function() {
alert(data);
};
menu.show(page);
};
})();

In the latter code sample:

  • Menu and MenuItem instances are created only once

  • Events and properties are set when showMenu is called, same instances are used

Using console.log statements

These statements can cause a big bottleneck in the JavaScript UI thread. These statements are unnecessary when running a published app. So make sure to remove them before publishing.

Other

  • When performing lazy loading on ListView, use onRowBind method.

  • onLoad method of a SwipeView's page is triggered when swipe occurs on Android. On iOS, onLoad method of all pages is triggered at the beginning. Please consider this while working with a SwipeView object.

Application Size

Users are usually reluctant to download an application that is too large. This section explains how to reduce the size of the application.

App resources can take up 70%-75% of the application. Resources can be a custom fonts, multimedia content or images. You can compress jpeg and png files without affecting the visible image quality using tools like pngcrush. It is also recommended to delete the unused resources.

In Android, you don't need to support all available densities based on the user base. Android densities are mdpi, hdpi, xhdpi, xxhdpi and xxxhdpi. If you have a data that only a small percentage of your users have devices with specific densities, consider whether you need to bundle those densities into your app or not. For more information screen densities, see Screen Sizes and Densities