Skip to main content
Version: Next

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:

FeatureiOSAndroid
File operationsFasterSlower
Image creation from fileFasterSlower
Blob operationsFasterSlower
Page CreationSlowerFaster
Property assignmentSlowerFaster

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.

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.

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

Splash Screen and Initialization Time

Splash screen will be shown until app.ts has finished Code Execution Phase. If your application initialization time is slow, consider the following points:

info

The Splash and app initialization flow on Smartface Emulator and published app are totally different. This section only covers the published app.

  1. Make sure there are not too much indexing and importing on the require time
  2. Try not to import all of the pages on the require time. Divide it into chunks if necessary.
  3. The third parties and tools you've acquired through NPM might cause the slowness (e.g. @faker/faker-js )

Dynamic Require on Splash Time

If you've checked all of those and the time is still slow for you, as a last resort you could use require instead of import on Pages.

routes/index.ts
import Page1 from 'pages/page1';
import { NativeRouter, NativeStackRouter, Route } from '@smartface/router';

const router = NativeRouter.of({
path: '/',
isRoot: true,
routes: [
NativeStackRouter.of({
path: '/pages',
routes: [
Route.of<Page1>({
path: '/pages/page1',
build(router, route) {
return new Page1(router, route);
}
})
]
})
]
});

Within this usage, the contents of top of Page1 will be executed since they are supposed to work on require time. If the contents take too much time to load, we can go with an alternative approach:

routes/index.ts
import { NativeRouter, NativeStackRouter, Route } from '@smartface/router';

const router = NativeRouter.of({
path: '/',
isRoot: true,
routes: [
NativeStackRouter.of({
path: '/pages',
routes: [
Route.of<Page1>({
path: '/pages/page1',
build(router, route) {
const Page1 = require('pages/page1').default;
return new Page1(router, route);
}
})
]
})
]
});

This way, the require time contents will load after the first page is executed. However, there are two caveats:

  1. You will lose typing and type check since you can't import the Page1 at the top of file. import type from 'pages/page1'; is a risky alternative, because import type might also invoke some code blocks.
  2. When you e.g. change the file name, the compiler will not mark the changes in error, so you have to check manually if there's a runtime error.

In summary, we are losing some benefits of Typescript. Therefore, use with caution.

UI Specific Points

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:

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.

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.

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
function pageConstructor() {
setupMapReadiness(page);
}

function onShow() {
tickMapReady();

this.mapReady(()=> {
//perform the service call
});
}

functon onMapViewCreate() {
tickMapReady();
}

function buttonPress() {
this.mapReady(() => {
//do any action for intreacting the map
});
}

function setupMapReadiness(page) {
let tick = 0;
let readyList = [];
this.tickMapReady = function tickMapReady() {
tick++;
if(tick >= 2 && readyList.length > 0) {
readyList.forEach(fn => fn.call(page));
readyList.length = 0;
}
};

this.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
}
}
}

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

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

ListView

  • When performing lazy loading on ListView, use onRowBind method.
  • Don't assign onScroll, onGesture and other scroll methods unless you have a specific use and need to them.

SwipeView

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.

ScrollView

ScrollView, ListView, GridView can be used to show vertical&horizontal(except listview) scrolling content. In ScrollView, all of the child components are created at the beginning while ListView and GridView will create the items when the items 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.

Usage Wise Tips

Property assignment

Property assignments will directly affect UI thread:

Sample Assignment
this.textBox1.text = "Smartface"; //assignment

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.

Sample Assignment
import Color from "@smartface/native/ui/color";

this.flexLayout.backgroundColor = Color.RED;
const myRed = Color.create("#FF0000");
this.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.

this.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

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.

this.flexLayout.dispatch({
type: "pushClassNames",
classNames: ".myFlexLayout",
}); //this makes the background color as red

this.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

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.

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)

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.

Caching

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

Without Caching
listView.onRowSelected = function (listViewItem, index) {
showMenu(data);
};

function showMenu(data) {
const menu = new Menu();
const menuItemCopy = new MenuItem({
title: "Copy",
});
menu.items = [menuItemCopy];

menu.headerTitle = data;
menuItemCopy.onSelected = function () {
alert(data);
};
menu.show(page);
}
With Caching
listView.onRowSelected = function (listViewItem, index) {
showMenu(data);
};

const showMenu = (function () {
// This block runs only once and variables are cached
const menu = new Menu();
const 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

Debugging/Development Wise Tips

When the applications are developed, it is inevitable that there will be some development specific code/logging actions to take. It will be necessary to take production and performance in mind.

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.

You can combine System.isEmulator with console.logs like:

if (System.isEmulator) {
console.log('development log: ', Error().stack);
}

Best Usage over Console on development

It is better to create another log function for your usage and use that instead:

lib/logger.ts

import System from '@smartface/native/device/system';
import loggerService from 'service/logger'; //Arbitrary path

enum LogType {
INFO = 'info';
LOG = 'log';
ERROR = 'error';
WARN = 'warn';
}

function logMapper(type: LogType): (...args: any[]) => void {
if(type === LogType.INFO) {
return console.info;
} else if(type === LogType.LOG) {
return console.log;
} else if(type === LogType.ERROR) {
return console.error;
} else if(type === LogType.WARN) {
return console.warn;
}
}

export default function logger(type: LogType, ...args: any[]) {
if(System.isEmulator) {
const printConsole = logMapper(type);
printConsole(...args);
}
else {
// If you have e.g logger service, you can locate them here. Make sure to not block the main thread.
loggerService.get(...args); //Notice how we didn't use promise or async/await. This request will go to the server in the background.
// Or, if you use Firebase you can also invoke Analytics there.
}
}

//usage:
// logger('log', Error().stack, " : ", "test");

Side note: You can also apply the same approach when it comes to errors.

Overriding Console Functions

Riskier approach can be done by overriding the console functions themselves(not recommended):

originalConsoleLog = console.log.bind(null);
console.log = function(...args: any[]) {
if(System.isEmulator) {
originalConsoleLog(args);
}
}