How we made our app 80% faster

Keeping Birdie’s app fast, powerful and dependable

Tim Robinson
Engineering at Birdie
9 min readOct 27, 2022

--

Image from https://www.birdie.care/

Did you know that an Internet utility bill doesn’t count as a valid proof of address when renewing an Irish driving license? I was so adamant that it would suffice (it didn’t), that I brought it along to my renewal anyway.

It’s no exaggeration to say that without an Internet connection, I wouldn’t be able to do my job, and I’d have to scrounge around for an entirely different skillset to produce any kind of income.

In the world that I’ve become accustomed to, a fast WiFi connection is vital. But when my Google-fuelled gratification stops being so instant, that’s when it becomes clear just how important that “fast” part is.

More broadly, the idea of instant gratification from any website or app — network connection or not — has become the norm, and anything that deviates from that expected standard can be a frustrating experience for users. We don’t notice it when things are running smoothly, and we can’t help but notice it when they’re not.

One study found that 61% of users expect a mobile app to start in 4 seconds or less, and an additional 37% of users said that any crashes, bugs or performance problems in a mobile application would make them think less of a company’s brand.

For a company who creates mobile apps, while good performance can lead to satisfied user and app downloads, poor performance will result in quick app abandonment. […]

The consequence of failing to meet user expectations is not only app abandonment — it also leads to a tarnished brand with lost revenue opportunities from both current and future users.

I’ve discussed before how automated testing can catch these kinds of experience-breaking bugs for a mobile application, but catching performance problems is whole different ball game.

And in some ways, it’s even more important.

Performance anxiety

For most mobile apps, the reason you want to keep it sturdy and performant is to avoid frustrating any of your users. Most people use apps because they’re convenient, or to socialise, or to entertain them on a long train journey. These apps aim to keep you coming back to them as often as possible, and optimising your experience whilst using them is a huge part of that plan to keep you engaged.

But for the Birdie app, things are different.

Our app is made specifically for care workers to use during their visits to elderly care recipients in their own homes, and allows instantaneous paper-free reporting on anything from meals to missed medications.

For us, things need to be smooth and performant not because we want to keep our users entertained, but purely because we want to help them minimise the time spent using the app. Every moment not spent waiting for something to load on their device is another moment they can spend tending to someone’s loved one.

Birdie’s app has been around for several years, and has spent those years being ceaselessly built upon. With a codebase accruing changes and gathering features for that long, it was inevitable that we started getting reports from some users that the app had begun to slow down.

It was time to act.

A needle in a poorly performing haystack

Our first and most pressing problem was a lack of data.

Because our developers mostly tested on emulators or their own fairly powerful devices, we didn’t have much of an insight into where the performance problems actually were.

On top of that, most of our feedback sessions were with care managers, all of whom worked mainly with the Birdie web portal, to oversee and organise day-to-day care, as opposed to the care workers themselves, who used the mobile app extensively to deliver that care.

Couple those problems with the fact that our in-app analytics didn’t include any worthwhile performance tracking, and it becomes clear that we were looking at implementing a shot-in-the-dark solution.

It’s likely no surprise then that one of the first things we wanted to do was start more accurately tracking and monitoring performance problems.

The Birdie app runs on React Native with Redux and Redux-Persist, so the tool we were on the lookout for had to work well with that setup and offer the deep-dive insights that we needed. After trying a few different tools, we ultimately settled on Datadog’s RUM toolkit, which allowed us to monitor performance at a very granular level, from the performance of specific Redux processes, to the average memory usage per screen.

But we couldn’t just wait for the data to come through as users slowly updated to the new version of the app with our shiny new monitoring setup — they were already experiencing the pain of degraded performance — we had to step in, even if we didn’t yet know the underlying problems.

Our Datadog RUM app monitoring dashboard

A thousand papercuts

Our first attempts at improving performance came in the form of a large collection of relatively minor changes in a lot of different places. The logic was that a lot of smaller problems could combine into a single unmanageable one, and that the app had started to suffer a death by a thousand papercuts.

Some key highlights in our performance-improving endeavours, we…

  • Froze our navigation stack when the app was deep into multi-stage forms or screens.
  • Made liberal use of React.useMemo on our components and memoised our Redux selectors to avoid any unnecessary screen re-renders.
  • Upgraded core dependencies such as react-navigation so that we could make use of native navigation stacks (@react-navigation/native-stack).
  • Experimented with moving from Redux to WatermelonDB for on-device data storage.

The vast majority of our changes made some difference, and remain part of the Birdie app today. But none of them were the panacea for performance problems that we sought so increasingly.

More data, more problems

Our biggest attempt at improving performance was to reduce the amount of data we were handling.

One of our app’s most crucial features is offline-first support, which allows carers to continue reporting on Birdie as usual without needing signal or keeping their mobile data on. It’s because of this functionality that, when you do have an Internet connection, the app downloads and stores all revelant visit and care information for the day.

If the carer’s agency was quite large, then the amount of data that the device would need to store was therefore also quite large, and so it was logical that reading and writing such a large amount of data could cause some serious performance problems.

This logic was especially sound when we considered that any data stored to the device for offline use would have to be stringified and sent across the React Native Bridge, to be stored by Redux Persist. From our preliminary performance monitoring, this act of stringifying and sending over large chunks of data looked like a real contender for our primary bottleneck.

Data going through the React Native Bridge
Data persisted to the device via redux-persist must pass through the React-Native Bridge

So we got to work. We implemented various means of giving control to the user to limit how much data their device downloaded for use in offline mode, cut back on when and how often we decided to persist data, and even experimented with entirely different means of sending things across the React Native Bridge.

And… in the end? There was virtually no noticeable difference.

Something else was still slowing everything down, and it was only once our theory about large datasets was disproven that we found exactly what we were looking for.

Ask for forgiveness, not Permissions

One thing that didn’t quite add up was that despite data reduction having no real impact at all, there was still a very clear correlation between agency size and performance issues. That was when we realised that there was one more thing that scaled with agency size: Permissions.

Birdie users — both on the app and on the care manager’s web portal — are assigned lists of certain permissions they’ve been granted, based on a combination of factors. If, for example, you’re a care manager at an agency that has the Visit Planning feature enabled, then one of your user permissions would be a manage_visits permission, which would signify that you as a user have the ability to add or modify scheduled visits as you see fit.

This permissions-based approach allows each user’s app experience to be tailored to them, with new features able to be displayed or removed simply by checking the user’s permissions.

When the Birdie app was first launched, this list of permissions numbered at a maximum of around 15 or 20 in total. By the time we were looking into this, for a large agency, this could easily number into the thousands.

Any time there were any checks in the app to see whether or not you had a certain permission, it had to scan through an enormous list to find out. And we checked your user permissions a lot.

A good example of the app checking permissions is our PermissionControl component, which checks a given permission before rendering any child components.

Imagine you were given a list of ten thousand different names, and someone was asking you every few seconds to find certain names on there. You’d get there eventually, but it would be a laborious and slow process. This was the same problem that our app was facing.

Now imagine that rather than a flat list of names you’re instead handed a book, which has a neatly ordered table of contents for your reference, and all names are grouped by last name and in alphabetical order. When someone asks you to find a name in your book, you know exactly what page to turn to and check.

We applied this exact logic to our list of permissions. Instead of keeping a flat array with every single permission in one list, we separated out the permissions into distinct groups and converted that long list into a lookup object.

Permissions transformed from a long list into a more optimised object

Once checking permissions was no longer a prohibitively expensive proceedure, the app’s performance problems melted away.

There was a massive downswing in loading times and a massive upswing in responsiveness. Users stopped reporting any performance problems at all. The monitoring that we’d managed to get in place reported incredible increases in speed wherever permissions were being checked, with some Redux selectors and rendering of components having increased in speed by 90%, leading to an overall 80% speed improvement on some devices!

A comparison between the two app versionson our testing agency with an incredible amount of data

That was it. We’d found our solution. But in a very unexpected place.

Reflections

Programming allows you to think about thinking, and while debugging you learn learning — Nicholas Negroponte

Ultimately, we learned a lot from our urgent foray into improving the performance of a React Native app, and I’d like to think we’ve carried those learnings forward into everything we’ve done with the app since.

The most important learning was, to quote one of our developers, that “performance improvements without tooling is always a guessing game”. Our logical estimations and assumptions about what was slowing the app down didn’t bear any fruit in the end, and it was only by eliminating those assumptions by trying them out did we stumble upon the actual answer.

We’re still using Datadog’s RUM toolkit, and we’ve now been empowered to become much more proactive in our work, rather than waiting for things to get noticeably worse before taking action. We can have full confidence in our app now that we have the data to back it up.

Since this improvement, and the addition of that monitoring, we’ve also devised a dedicated team to look after the mobile app, and even a separate team focused entirely on the caregivers’ experience with Birdie.

In fact, my colleague who is now the lead of that very mobile app squad, has given a talk at a React Native conference on this very topic, and I highly recommend you check it out!

Final thoughts

All of this is to say, never underestimate the power of monitoring. Never underestimate the usefulness of having real, tangible data for debugging. And never underestimate how much impact a small fix can make, if you’re willing to put the effort in to find it.

You might just make your users’ worlds a little brighter, and just a tad faster.

We’re hiring for a variety of roles within engineering. Come check them out here:

https://www.birdie.care/join-us#jobs-live

--

--