Flutter vs React Native – what to choose in 2020

RN vs Flutter

Background

At Evenset, we make custom software for healthcare applications. A big part of our work is developing mobile apps to serve as companions for medical devices, and our go-to framework has always been React Native. We do try to keep things fresh, so I was asked to take a quick look at Flutter. This is what I found

Why do it ourselves?

There are plenty of comparisons online to choose from (and now there’s one more), but after looking around we decided it would be best to investigate on our own. The first performance comparison we found measured computationally difficult work: the Gauss-Legendre algorithm and the Borwein algorithm. The overall conclusion was that react native was slower than flutter. However, this doesn’t represent the work we do – it’s rare that our apps rely on doing any complex computations efficiently. When possible, such work is done server side. When we talk about performance, we really have three factors in mind:

  • How fast the app launches
  • How fast the app renders a new screen
  • How smooth animations look

Computation tests don’t help us with this very much. Application code is rarely the main factor in our apps’ performance (and when it is, we fix it). We decided it would be valuable to test for exactly what we were interested in. Additionally, getting into the code myself would help me get a better understanding of how using flutter feels, which is just as important as performance; if we can’t use flutter correctly, any theoretical performance benefits are irrelevant.

First Steps

The first step was to create a basic flutter app and to replicate it with react native. We went for something just *slightly* more complicated than Hello World: a text field with a submit button. When you click the submit button, three things happen: the text box clears, a modal animates into view with the text on it, and the text is added to a list of messages displayed above the screen. I didn’t have specific visual requirements, but it had to not look raw and unstyled.

This all came together very quickly with Flutter. The starter app provided us with a themed view that had a floating action button – the rest of it, I got rid of. Adding in a text input and a message list was trivial. The alert was a little harder – I went with a modal that faded in and scaled at the same time – but some googling produced this:

String msgClone = messageController.text;
showGeneralDialog(
  barrierColor: Colors.black.withOpacity(0.5),
  transitionBuilder: (context, a1, a2, widget) {
    return Transform.scale(
      scale: a1.value,
      child: Opacity(
        opacity: a1.value,
        child: widget,
      ),
    );
  },
  transitionDuration: Duration(milliseconds: 300),
  barrierDismissible: true,
  barrierLabel: '',
  context: context,
  pageBuilder: (context, animation1, animation2) {
    return AlertDialog(
      shape: OutlineInputBorder(
        borderRadius: BorderRadius.circular(16.0)
      ),
      title: Text('Message Received'),
      content: Text(msgClone),
    );
  },
);

A couple things I noticed when creating this component:

In the tutorial I saw, pageBuilder was an empty function, and the entire modal was rendered inside the transitionBuilder parameter. It seems that the return value of pageBuilder is simply passed to transitionBuilder as the fourth argument. These parameters have a clear separation of purpose, but right now it seems redundant.

There seem to be some overcomplicated elements here. For example, transitionDuration: Duration(milliseconds: 300). The Duration class is roughly equivalent to a moment.js duration, and moment is something we always end up needing in react native, but I don’t see a good reason to use the class over transitionDuration: 300. I had the same impression about borderRadius: BorderRadius.circular(16.0), but it turns out this can be used to create some more complex geometries. Still, it would be nice to just be able to do borderRadius: 16.0.

Overall, this was much easier than doing the same thing in React Native, even with my experience in RN development. This is probably mostly because flutter has a default appearance which I tried to replicate in react, so this is not an entirely fair comparison. However, it’s worth noting that react native, out of the box, doesn’t really have a default appearance. A mostly unstyled React Native app would appear, well, mostly unstyled. My first impression was that flutter would be good at giving us reasonable-looking products, but it could become harder to use for pixel-perfect design matching.

I had a lot of difficulty getting the modal to behave the way I wanted it to in React Native. It ended up looking like this:

<Modal
  transparent
  visible={showModal}
  onRequestClose={() => setShowModal(false)}
  animationType="fade">
  <View style={styles.modalContainer}>
    <TouchableOpacity
      style={styles.touchContainer}
      onPress={() => setShowModal(false)}
      activeOpacity={1}
    >
      <View style={styles.modalBody}>
        <Text style={styles.modalTitle}>Message Received</Text>
        <Text style={styles.modalText}>
          {messages.length ? messages[messages.length - 1] : ''}
        </Text>
      </View>
    </TouchableOpacity>
  </View>
</Modal>

...

modalContainer: {
  flex: 1,
  backgroundColor: 'rgba(0, 0, 0, 0.5)',
  display: 'flex',
  flexDirection: 'column',
  alignItems: 'stretch',
  justifyContent: 'center',
},
touchContainer: {
  flex: 1,
  display: 'flex',
  flexDirection: 'column',
  alignItems: 'stretch',
  justifyContent: 'center',
},
modalBody: {
  marginHorizontal: 48,
  backgroundColor: 'white',
  borderRadius: 16,
  display: 'flex',
  flexDirection: 'column',
  alignItems: 'flex-start',
},
modalTitle: {
  marginHorizontal: 18,
  marginVertical: 12,
  fontSize: 24,
  fontWeight: '600',
  textAlignVertical: 'center',
},
modalText: {
  marginHorizontal: 18,
  marginBottom: 12,
  fontSize: 18,
  textAlignVertical: 'center',
},

This is a lot more code, but that’s understandable as it’s trying to copy the Flutter code. However, some of this is pretty standard React Native. For example, the TouchableOpacity used to create the “dismiss on background click” effect – this is a standard functionality for a popup, not just something I did to match flutter.

Subjectively, I really prefer flutter’s approach to popups here – having a modal always exist and displaying it with visible={showModal} is less intuitive than calling showGeneralDialog and passing it a widget to render. This is how react native handles alerts, but an alert can’t display an arbitrary component, so this was a welcome change with flutter.

I gave up on trying to make an exact copy of the flutter animation in react native. The modal component allowed me to copy the opacity animation, but the scale proved to be too much effort for this review. Normally, such animations are possible using React Native’s Animated API, but the modal is its own beast.

Performance

The first performance test was simple: rendering a big page. To do this, I made a modification that allowed us to enter a number into the text field, and then it would add that many rows of text. Flutter rendered 5000 rows in about 6 seconds, compared to 9 for React native. This was useful information, but it didn’t tell us much that the computational tests didn’t. The screen was still only rendered once, so I didn’t know exactly what I had measured – it could just be the speed of a loop in js vs dart.

For the second test, I wanted to test how long it took to render thousands of times, rather than just thousands of rows. To do this, I needed to set off a render loop. Entering a number would add that many “required rows”, and any time the page rendered without enough required rows, it would add a row and re-render. In flutter, this was easy enough:

if (_messages.length < (_requiredLength)) {
  SchedulerBinding.instance.addPostFrameCallback((_) => {
    setState(() {
      _messages.add(_messages.length.toString());
      if (_messages.length == _requiredLength) {
        showDialog(
          context: context,
          builder: (BuildContext bctx) {
            return AlertDialog(
              title: new Text("Done")
            );
          },
        );
      }
    })
  });
}

react was a bit harder since, to its credit, it has safeguards against render loops. The end result was this:

useEffect(() => {
  if (messages.length < requiredLength) {
    setTimeout(() =>
      setMessages([...messages, messages.length])
    );
    if (messages.length + 1 === requiredLength) {
      Alert.alert('done');
    }
  }
}, [messages.length, requiredLength]);

setTimeout is required because without it the rows didn’t render one at a time – if a state change is made during a useEffect call, the state change can be resolved before the render.

This test gave us different results: react native was faster. It took 40 seconds for react native, compared to 49 for flutter. Next, we wanted to see how this scaled with the complexity (size) of the page. So I took measurements every 1000 renders to see if the app slowed down. The results:

total time (seconds):
# of rows   Flutter   React Native
1000        42        27
2000        133       91
3000        278       197
4000        474       340
5000        723       522

section time (seconds):
row range   Flutter   React Native  Flutter:RN Ratio
1-1000      42        27            1.56
1001-2000   91        64            1.42
2001-3000   145       106           1.37
3001-4000   196       143           1.37
4001-5000   249       182           1.37

which is a pretty cool result. This is the result I place the most stock in, because we know it isn’t dominated by between-render effects like the use of setTimeout; those would be less significant compared to render time as the renders slowed. The convergence to 1.37 is also valuable information: flutter takes about 40% longer to render complex views compared to react native. Our numbers suggest (but don’t prove) that this ratio would be stable for longer render times – our slowest renders were in flutter, and probably took around 300ms (the average time for the 4000-5000 batch was 250ms, but the later ones would have been slower)

Post Your Comments

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