4 Lesser-Known Performance Tips to Improve User Experience in React Native Apps

There is more you can do than just optimize the frequency of the renders

Andre Pimenta
Feb 13 · 6 min read
Photo by Émile Perron on Unsplash.

As the CTO of a company that develops a React Native app with tens of thousands of active users every day, one of my biggest concerns is making sure that the app provides a great experience for all users — no matter how good or bad their mobile devices are.

You have probably heard that the performance of a React Native or React app depends directly on the number and frequency of renders. You can read about it on the official documentation. Avoiding unnecessary renders is the first thing you should take into account when developing a React or React Native app. You can read more about this through a quick Google search, so I am not going to cover this.

Instead, I am going to give you extra performance tips that will make a significant difference in the performance of your app. From my experience, there are always four problematic components. When optimized, they make the app feel much faster and in general more responsive.


The 4 Most Problematic Components

1. Navigation: Slow animations

2. Lists: Take a lot of memory

3. Long-running functions: Clog the JavaScript thread

4. Persisting data: Slows down the entire app

Let’s go through these components one by one, identify each problem, and find the solution.


Navigation

A glitchy or slow navigation experience is something your users will notice, and it’s reason enough for the app to lose their attention. What you should be aiming for is a smooth animation transition between screens, and that is not always the case.

On screens with a lot of components, you may find there is a significant delay between the time the user pressed the button and the transition to the new screen.

This depends on the library you use for navigation. With React Navigation, for example, the screen is rendered off-screen and the animation only happens afterward. So what you should do is aim for a fast first render and only render the big list of components after the animation.

On screens with a lot of business logic, you may find that the animation is glitchy or janky.

This also depends on the library you use for navigation, but if it uses JS-accelerated animations, the business logic can cause it to drop frames because they both run on the same JavaScript thread. So what you should do is sort out your business logic only after the animation is completed.

Basically, what you want is to know when the animation is completed. There are three options that can be used to accomplish this:

Let’s now take a look at how you can implement each of them:

/*  Option 1: Using InteractionManager */
componentDidMount() {
// You can do purely async functions here
fetchData()


InteractionManager.runAfterInteractions(() => {
// Navigation is done animating
// Synchronous function
const data = processData()
// Render the screen
this.setState({
loading: false,
data
})
})
}
/* Option 2: Using didFocus */
componentDidMount() {
// You can do purely async functions here
fetchData()

navigation.addListener('didFocus', () =>{
// Navigation is done animating
// Synchronous function
const data = processData()
// Render the screen
this.setState({
loading: false,
data
})
})
}
/* Option 3: Using setTimeout */
componentDidMount() {
// 1. You can do purely async functions here
fetchData()
setTimeout(() => {
// Navigation got some frames for animating
// Synchronous function
const data = processData()
// Render the screen
this.setState({
loading: false,
data
})
}, 1)
}
/* Render */
render() {
/* When loading is true, render just a very simple loading view */
/* When loading is false, render the components */
}

Bonus tip

Because the navigation keeps the screens on the stack and does not dismount them, the app memory can start to increase a lot. You can use willBlur (lose focus) to dismount or render simpler components (loading screen) for those hidden screens.


Lists

Lists tend to take a lot of memory and can cause JS and UI frames to drop. If your app uses navigation or a swiper, you probably have lists that are not even being displayed to the user but still take a lot of memory resources because they are mounted somewhere. Also, even if your list is currently being displayed to the user, they may not scroll down the list, so it doesn’t matter if all of the items of the list are mounted.

Basically, what you want is to render the lowest amount of list items possible while making sure that when the user sees the list, at least the first few items are shown.

If the list is currently being displayed on the active screen, you can pass only the first three items of the list array.

If the list is currently being displayed, you can control how many items are rendered initially.

/* If your screen loses focus or is not visible, you can manipulate the data being rendered */const data = this.isVisible ?
this.state.bigList
: this.state.bigList.slice(0, 3)

<FlatList
data={data}
/* If your list is always visible, you can try adding */
initialNumToRender = {5}
...
/>

Long-Running Functions

One of the downsides of React Native is that every JavaScript code runs on a single JavaScript thread. If the JavaScript thread is clogged, you may notice that the entire app is generally slow or completely unresponsive.

So first of all, you should not have any long-running synchronous function in React Native at all. But sometimes it’s not possible to avoid that. In that case, you can use React Native workers, which takes some time to implement. You could also break your long-running function into batches of small steps. Those batches should then run one after another separated by a few milliseconds.

Those few milliseconds let the JavaScript thread have some frames for other actions. This will make the long-running function a little bit slower, but your app will be fully responsive again. And the best part is that this can be achieved very easily with the use of await.

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))// How many steps your heavy function should run before it goes to sleep
const sleepFrequency = 200
// How much time in ms your heavy function should sleep
const sleepTime = 10
longRunningFunction()
for (let i = 0; i < enormousNumberOfSteps; i++) {
stepProcessing() /* Add this on your intensive functions */
if (i % sleepFrequency === 0 && i > 0){
await sleep(sleepTime)
}
}
}

Bonus tip

If the user leaves the screen or calls the long-running function again with different parameters, you should cancel the previous one in the middle of running by checking for different parameters and exiting the function before it ends.


Persisting Data

If you need to persist data, you may find that saving data of a huge size very frequently makes your app a lot slower. For example, I deal with real-time data that needs to be saved to the user device. So what I do is use debounce. Debounce delays invoking the save function until after X milliseconds have elapsed since the last time it was invoked. It batches what needs to be saved and then saves it all at once. If you use React Persist, this is easy to do:

const persistConfig = {
...
//Add debounce here
debounce: 1000
};const persistedReducer = persistReducer(persistConfig, rootReducer)

Bonus tip

You can also only save your app data at certain points (e.g. right before the user leaves the app).


Conclusion

I hope this helped you improve your app!

If you need help with development or have any questions, feel free to post a comment or contact me at andre@andrepimenta.io.

Better Programming

Advice for programmers.

Thanks to Zack Shapiro

Andre Pimenta

Written by

CTO • Senior Full Stack Developer • linkedin.com/in/andrepimenta7

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade