Cross-platform iOS development with JavaScript and React Native. Part one

JavaScript running iOS applications can have the same or close to native performance. Do you believe this or not? Let's find out.

JavaScript running iOS applications can have the same or close to native performance. Do you believe this or not? Let’s find out.

The first cross-platform framework that comes to mind is React Native. It is a framework for cross-platform development on iOS and Android. It uses JavaScript and TypeScript which are simple and robust languages, but these languages are not famous for its performance. The first concern using cross-platform apps is a performance, low FPS (frames per second) and battery drain due to non-optimized software.

JavaScript is a scripting language and it means there is no compilation just interpretation so definitely it lacks compilation optimization. But going deeper we can find that on iOS JS code runs on JavaScriptCore (Apple, FB), which is the framework for evaluation JavaScript in iOS apps, obviously from its name. The runtime compilation can be possible with some engines running JS – for example, V8. On iOS, the JavaScriptCore Engine doesn’t support J-I-T (just-in-time) compilation.

The simple architecture includes JSContext where the magic happens, the interoperability between native code and JavaScript.

import JavaScriptCore

let context = JSContext()!
context.evaluateScript("let value = 5 + 10")
let value : JSValue = context.evaluateScript("value")

print(value) // 15

With JSValue we can pass objects back and forward from native to the scripts. It supports from JS side: numbers, strings, arrays, objects, and functions, null and undefined. The framework converts objects from Objective-C (Swift) to JavaScript and vice versa. There is pretty flexible and complete interoperability.

Objective-C (Swift) → JavaScript Types

nil → undefined

NSNull → null

NSNumber → Number, Boolean

NSString (String) → String

NSDictionary (Dictionary) → Object

NSArray (Array) → Array

NSDate → Date

Structures (NSRange, etc) → Object

Objective-C block (closures on Swift) → Functions

The code below shows an example of JavaScript code for instantiation of UILabel in comparison with a native code. The measuring was made 10 times each and the average value is taken.

import JavaScriptCore
import UIKit

func timeToExecute(block: () -> ()) -> CFTimeInterval {
    let start = CACurrentMediaTime()
    block();
    let end = CACurrentMediaTime()
    return end - start
}

let context = JSContext()!

func createLabelNative() -> CFTimeInterval {
    return timeToExecute {
        let label = UILabel(frame: CGRect.init(x: 0, y: 0, width: 200, height: 50))
        label.text = "Hello, World."
    }
    
}

func createFromJS() -> CFTimeInterval {
    return timeToExecute {
        let script = "let frame = {x: 0, y: 0, width: 200, height: 50}; let uiElementClass = 'UILabel'"
        context.evaluateScript(script)
        let frameValue : JSValue = context.evaluateScript("frame")
        let frameDict = frameValue.toDictionary()!
        let uiElementClass : JSValue = context.evaluateScript("uiElementClass")
        
        if uiElementClass.toString() == "UILabel" {
            let x = frameDict["x"] as! NSNumber
            let y = frameDict["y"] as! NSNumber
            let width = frameDict["width"] as! NSNumber
            let height = frameDict["height"] as! NSNumber
            let frame = CGRect.init(x: x.intValue, y: y.intValue, width: width.intValue, height: height.intValue)
            let label = UILabel(frame: frame)
            label.text = "Hello, World."
        }
    }
}

Results of measuring:

  • 10 iterations overall time native = 0.34 overall time JS = 0.24. JavaScript works faster 0.0094 seconds 42.26 % faster
  • 100 iterations overall time native = 2.09 overall time JS = 2.38. Native works faster 0.0028 seconds 13.69 % faster
  • 1000 iterations overall time native = 55.29 overall time JS = 59.3. Native works faster 0.004 seconds 7.3 % faster.

It is a simple example just to illustrate that script interpretation is given with a low time cost O(c). The more iterations we have the less difference between the native and JavaScript plus Native.

The 10 iterations show that interpretation of JS with native code is faster than native itself. It is a statistical error. As we all know the power of native. The 100 and 1000 iterations show 13.7 and 7.3 % respectively that native is faster. Again, this experiment is just a try-out of JavaScript on iOS.

The React Native communicates to native code with means of C++/Objective-C wrappers. So the speed can be very close to native with the difference in interpretation of JS code. One of the best features of React Native is sharing code between platforms (iOS and Android) and the support of native user interface elements. You can read about RN on its official page and see its advantages over native development.

Now it is time to compare the performance of React Native and native rendering of UI elements. And see how much is this framework “native” in terms of performance.

The current version of RN is 0.57. Installing the framework:

brew install node
brew install watchman
npm install -g react-native-cli
react-native init BenchMark

Starting the project:

cd BenchMark
react-native run-ios

React Native project has a SectionList (as UITableView analog), an Image, TextInput, Button and Text elements (Components). Those elements are rendering the corresponding UI to the native.

The code below used a basic RN app for testing.

import React, { Component } from 'react';
import { StyleSheet, Text, View, ListView, SectionList, Image, TextInput, Button, WindowedListView} from 'react-native';

var dataSource = []

for (var i = 0; i < 100; i++) {
  dataSource.push("element")
}

type Props = {};
export default class App extends Component<Props> {

  constructor(props) {
    super(props);

    const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
    this.state = {
      dataSource: ds.cloneWithRows(['row 1', 'row 2', 'row 3']),
    };
  }


  render() {
    return (
      <View style={styles.container}>
        <SectionList
          sections={[
            { title: '', data: dataSource },
          ]}
          renderItem={({ item }) => 
            <View style={styles.item}>

              <Image source={require('./apple.jpg')} style={{
                width: 44,
                height: 44,
                resizeMode: 'contain',
              }} />
              <TextInput placeholder="input" style={styles.input}></TextInput>

              <Text>Text element</Text>
              <Button title="Button" />

          </View>
          
        }
          renderSectionHeader={({ section }) => <Text style={styles.sectionHeader}>{section.title}</Text>}
          keyExtractor={(item, index) => index}
        />
      </View>
    );
  }
}


const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: "row",
    justifyContent: 'center',
    alignItems: 'center',
    marginTop: 10,
    width: '100%',
    height: 200
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    marginBottom: 5,
  },
  item: {
    flex: 5,
    flexDirection: 'row',
    height: 45,
    justifyContent: 'center',
    alignItems: 'center'
  },
  input: {
    marginLeft: 15,
    marginRight: 15,
  },
});

 

The iOS native project consists of a UITableView with a custom cell where is shown an image, a text field, and a button.

We compare native iOS with React Native in CPU load, frames per second, memory and energy consumption. For test purposes, I scroll the table view as users usually do approaching news feed.

 

 

 

 

Native project Results: The CPU load was circa 15% when heavy scrolling happens.

The GPU load was measured with Core Animation profiling tool where we measure the frames per second. And in this measurement, we see values from 28 to 60 FPS. The zero values are shown when the rendering is not happening. The GPU Hardware utilization is from 4 to 10%. The memory used was 16 MB.

React Native Results: The CPU load is circa 40% and you can see from the picture above that more threads involved when scrolling along with main in contrary with the native implementation.

The GPU load is quite similar ranging from 27 to 59 FPS, GPU utilization is from 4 to 12%. Similar results to the native and that’s why it’s called React Native. Memory is 117 MB according to loading many libraries including RN itself. Energy consumption is low in this test though it is higher than on native.

The native code obviously outperforms React Native but in GPU utilization it has the similar performance which is the most important part of the app. The CPU load is much higher and it definitely needs optimization in critical places and it is really possible with Native modules. CPU load and memory usage are given tradeoffs for a code sharing and the interpretation of scrips along with a larger number of libraries in cross-platform applications.

As a conclusion, I found that a JavaScript and React Native are appropriate tools for cross-platform code especially with the embedded framework as JavaScriptCore and optimizations for native rendering. RN shows good performance in GPU consumptions on par with native frames per second. As a tradeoff, we can see memory and energy consumption.  In the next articles, we’ll go in-depth with React Native and its patterns.

One thought on “Cross-platform iOS development with JavaScript and React Native. Part one

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.