A few weeks ago I wrote a blog post comparing the performance of a simple timer app written as a native application then rewritten in both React Native and Flutter. In that blog post, I came to the conclusion that the React Native and Flutter implementations had roughly similar performance metrics. However, a helpful engineer from the Flutter team pointed out that the Flutter implementation was written particularly inefficiently, so in this blog post we’ll correct that issue and rerun our tests!
First, as a recap, let’s look at the Flutter code we wrote in the last blog post:
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]),
),
)
);
}
}
Let’s walk through the code. We have a StatelessWidget
called MyApp
that
builds a StatefulWidget
called MyHomePage
. MyHomePage
consists of a
Scaffold
, which is a kind of container class used to show different material
design oriented widgets, and a centered Text
widget. Every 10 milliseconds we
reset the state with updated timer information and redraw the widget tree.
Herein lies the problem - we’re not just redrawing the Text
widget, which is
all that we actually care about - we’re also redrawing the entire scaffold. So
every ten milliseconds we’re doing a lot more work than necessary. We can easily
fix this by by moving the Scaffold
and Center
widgets up into our stateless
MyApp
widget. Applying that adjustment results in the following:
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: new Scaffold(
body: new Center(
child: new TimerWidget(),
)),
);
}
}
class TimerWidget extends StatefulWidget {
@override
TimerWidgetState createState() {
return new TimerWidgetState();
}
}
class TimerWidgetState extends State<TimerWidget> {
int _startTime = new DateTime.now().millisecondsSinceEpoch;
int _numMilliseconds = 0;
int _numSeconds = 0;
int _numMinutes = 0;
@override
Widget build(BuildContext context) {
return new Text(
sprintf("%02d:%02d:%2d", [_numMinutes, _numSeconds, _numMilliseconds]),
);
}
@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();
});
});
}
}
It’s worth noting at this point that in the original article the React Native implementation was actually built with this optimization already applied. As a refresher, here’s the React Native implementation:
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',
}
});
We’ve already separated out a Timer
component, so this implementation won’t
suffer from the same performance flaw that the Flutter implementation did.
Now that we’re on a more level playing field, let’s rerun our experiment.
Old Flutter results on the Pixel
Updated Flutter results on the Pixel
React Native results on the Pixel
Native results on the Pixel
We knocked our CPU utilization on the Flutter implementation down about 3%, and had a minor reduction in memory usage as well. Not bad for a tiny amount of work! At this point, the Flutter implementations CPU utilization is comparable to the native application and considerably lower than the React Native implementation - the memory footprint, however, is still high.
Now let’s take a look on the Nexus 5X
Old Flutter results on the Nexus 5X
Updated Flutter results on the Nexus 5X
React Native results on the Nexus 5X
Native results on the Nexus 5X
Again, we shaved off about 3% CPU utilization with our performance enhancements and brought the Flutter implementation closer in line with the native implementation. Memory usage also went down quite a bit, from 31MB on average to 23MB, beating out React Native’s 27MB memory usage. It’s still a fair bit higher than the 14MB the native app is utilizing though.
Updated Conclusion
This is still a tiny test, but with our updated data I’m becoming more excited about the prospect of Flutter being a cross platform stack that’s more performant than React Native. I’m still not willing to make a judgement call one way or the other about which framework is faster in the real world, but I’m excited to watch Flutter continue to develop!
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.