A few weeks after I posted this blog post, a helpful engineer from the Flutter team reached out to me about some inconsistencies between the approaches I took in the React Native implementation vs the Flutter implementation. After fixing the discrepancy there was a meaningful difference between the performance noted in this article and the new results. You can find the updated findings and code in the take two version of this blog post.
It’s a difficult decision deciding whether your company’s mobile app should be a true native application or employ a cross platform approach like React Native or Flutter. One factor that often comes into play is the question of speed - we all have a general sense that most cross platform approaches are slower then native, but the concrete numbers can be difficult to come across. As a result, we’re often going with a gut feeling rather than specific numbers when we consider performance.
In the hopes of adding some structure to the above performance analysis, as well as a general interest in how well Flutter lives up to its performance promises, I decided to build a very simple app as a native app, a react native app, and a flutter app to compare their performances.
The app
The app that I built is about as simple as it can get while still being at least somewhat informative. It’s a timer app - specifically, the app displays a blob of text that counts up as time goes on. It displays the number of minutes, seconds, and milliseconds that have passed since the app was started. Pretty simple.
Here’s an example of its starting state:
And here’s an example after one minute, 14 seconds and 890 milliseconds has passed:
Riveting.
But why a timer?
I chose a timer app for two reasons:
- It was easy to develop on each platform. At its core this app is a text view of some type and a repeating timer. Pretty easy to translate across three different languages and stacks.
- It gives an indication of how efficient the underlying system is at drawing something to the screen.
Let’s take a look at the code
Luckily, the app(s) are small enough that I can add the relevant sections right here.
Native Android
Here’s the main activity of the native Android app:
class MainActivity : AppCompatActivity() {
val timer by lazy {
findViewById<TextView>(R.id.timer)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initTimer()
}
private fun initTimer() {
val startTime = elapsedRealtime()
val handler = Handler()
val runnable: Runnable = object: Runnable {
override fun run() {
val timeDifference = elapsedRealtime() - startTime
val seconds = timeDifference / 1000
val minutes = seconds / 60
val leftoverSeconds = seconds % 60
val leftoverMillis = timeDifference % 1000 / 10
timer.text = String.format("%02d:%02d:%2d", minutes, leftoverSeconds, leftoverMillis)
handler.postDelayed(this, 10)
}
}
handler.postDelayed(runnable, 1)
}
}
React Native
Here’s our App.js
file for the React Native app:
export default class App extends Component {
render() {
return (
<View style={styles.container}>
<Timer />
</View>
);
}
}
class Timer extends Component {
constructor(props) {
super(props);
this.state = {
milliseconds: 0,
seconds: 0,
minutes: 0,
}
let startTime = global.nativePerformanceNow();
setInterval(() => {
let timeDifference = global.nativePerformanceNow() - startTime;
let seconds = timeDifference / 1000;
let minutes = seconds / 60;
let leftoverSeconds = seconds % 60;
let leftoverMillis = timeDifference % 1000 / 10;
this.setState({
milliseconds: leftoverMillis,
seconds: leftoverSeconds,
minutes: minutes,
});
}, 10);
}
render() {
let { milliseconds, seconds, minutes } = this.state;
let time = sprintf("%02d:%02d:%2d", minutes, seconds, milliseconds);
return (
<Text>{time}</Text>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
});
Flutter
And finally here’s our Flutter main.dart
file:
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _startTime = new DateTime.now().millisecondsSinceEpoch;
int _numMilliseconds = 0;
int _numSeconds = 0;
int _numMinutes = 0;
@override
void initState() {
super.initState();
Timer.periodic(new Duration(milliseconds: 10), (Timer timer) {
int timeDifference = new DateTime.now().millisecondsSinceEpoch - _startTime;
double seconds = timeDifference / 1000;
double minutes = seconds / 60;
double leftoverSeconds = seconds % 60;
double leftoverMillis = timeDifference % 1000 / 10;
setState(() {
_numMilliseconds = leftoverMillis.floor();
_numSeconds = leftoverSeconds.floor();
_numMinutes = minutes.floor();
});
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new Text(
sprintf("%02d:%02d:%2d", [_numMinutes, _numSeconds, _numMilliseconds]),
),
)
);
}
}
Each app follows the same basic structure - they all have a timer that repeats every ten milliseconds and recalculates the amount of minutes/seconds/milliseconds that have elapsed since the timer was started.
How are we measuring the performance?
For those unfamiliar with Android development, Android Studio is the
editor/environment of choice for building Android apps. It also comes with a
helpful series of profilers to analyze your application - specifically, there’s
a CPU profiler, a memory profiler, and a network profiler. So we’ll use those
profilers to judge performance. All of the tests are run on thoughtbots Nexus
5X and my own personal first generation Google Pixel. The React Native app will
be run with the --dev
flag set to false
, and the Flutter app will be run
in the profile
configuration to simulate a release app rather than a JIT
compiled debug app.
Show me some numbers!
Time for the interesting part of the blog post. Let’s take a look at the results when run on the thoughtbot office Nexus 5X.
Native results on the Nexus 5X
React Native results on the Nexus 5X
Flutter results on the Nexus 5X
The first thing these results make clear is that a native Android app trumps both the React Native and Flutter apps by a non trivial margin when it comes to performance. CPU usage on the native app is less than half that of the Flutter app, which is still less CPU hungry than the React Native app, though by a fairly small margin. Memory usage is similarly low on the native app and inflated on both the React Native and Flutter applications, though this time the React Native app eked out a win over the Flutter app.
The next interesting takeaway is how close in performance the React Native and Flutter applications are. While the app is admittedly trivial, I was expecting the JavaScript bridge to impose a higher penalty since the application is sending so many messages over that bridge so quickly.
Now let’s take a look at the results when tested on a Pixel.
Native results on the Pixel
React Native results on the Pixel
Flutter results on the Pixel
So, right off the bat I’m surprised about the significantly higher CPU utilization on the Pixel. It’s certainly a more powerful (and in my opinion, much smoother) phone than the Nexus 5X, so my natural assumption would be that CPU utilization for the same application would be lower, not higher. I can see why the memory usage would be higher, since there’s more memory on the Pixel and Android follows a general “use it or lose it” strategy for holding onto memory. I’m interested in hearing why the CPU usage would be higher if anyone in the audience knows!
The second interesting take away here is that Flutter and React Native have diverged heavily in their strengths and weaknesses vs their native counterpart. React Native is only marginally more memory-hungry than the native app, while Flutters memory usage is almost 50% higher than the native app. On the other hand, the Flutter app came much closer to matching the native apps CPU usage, whereas the React Native app struggled to stay under 30% CPU utilization.
More than anything else, I’m surprised by how different the results are between the 5X and the Pixel.
Conclusion
I feel confident in saying that a native Android app will perform better than either a React Native app or a Flutter app. Unfortunately, I do not feel confident in saying that a React Native app will out perform a Flutter app or vice versa. Much more testing will need to be done to figure out if Flutter can actually offer a real world performance improvement over React Native.
Caveats
The profiling done above is by no means conclusive. The small series of tests that I ran cannot be used to state that React Native is faster than Flutter or vice versa. They should only be interpreted as part of a larger question of profiling cross platform applications. There are many, many things that this small application does not touch that affect real world performance and user experience. It’s also worth pointing out that all three applications, in debug mode and release mode, ran smoothly.
Want to learn more about mobile development?
thoughtbot can help accelerate your product development with proven best practices and processes derived from our 20 years of software development experience. Interested? Learn how thoughtbot can help grow your mobile application.