Examining performance differences between Native, Flutter, and React Native mobile development: Take two.

Alex Sullivan

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!