React Native — Making your app fast again

Gaurav Arora
6 min readAug 11, 2018

--

React Native is fun. If you have come from a native iOS or Android background, you would have realised its powers. Creating an app is super easy & uncomplicated. However, its equally easy to screw your app and make it slow and laggy — especially if you have a large app with complex architecture, state management, deeply nested components.

Even on a complex app, achieving 60 fps with React Native isn’t a rocket science. In this article I’ll be sharing common pitfalls that leads to a slow & janky app and ways you can avoid it.

1. Have mercy on constructor

I was reviewing a code of my peer when I found something like this:

class MyFavoriteComponent extends Component {

constructor(props) {
super(props)
performSomeLongRunningOperation();
}

}

See the problem here? Never perform something that takes time on constructor or componentWillMount. Prefer moving it to componentDidMount

2. Count your re-renders

Unnecessary re-renders is the #1 reason why most React Native apps are slow. Use tools like why-did-you-update or add simple breakpoint or counter in render() to monitor your re-renders and optimise them. Also, we’ll discuss below the reasons how you can avoid unnecessary re-rendering.

why-did-you-update

PS: Re-rendering of a component, doesn’t necessarily means the entire component will be re-drawn on native again. Since, JS first computes a virtual DOM or component tree, which later goes through a diff’ing engine — and only the final diffs are redrawn in native. However, avoiding the unnecessary re-rendering computation could boost our performance noticeably.

3. Be careful with functional props

This is #1 Reason for unnecessary component updates. We all must have seen codes like this:

class MyComponent extends Component {

render() {
return (
<SomeComplexComponent
prop1="Hey, I'm prop1"
prop2="Hey, I'm prop2"
onPress={(id) => doSomething(id)}/>
);
}

}

Problem: Every time MyComponent is re-rendered by its parent SomeComplexComponent will be re-rendered as well — even when all props for SomeComplexComponent remain the same — causing an unnecessary re-render computation.

Reason: onPress contains an arrow function. So, every time render() of MyComponent is called — a new reference of onPress is created — since, its an arrow function. Thereby, forcing SomeComplexComponent to re-render for no reason.

How to avoid: Memoize or simply avoid creating arrow functions inside render likes this:

class MyComponent extends Component {

render() {
return (
<SomeComplexComponent
prop1="Hey, I'm prop1"
prop2="Hey, I'm prop2"
onPress={this.doSomething}/>
);
}

doSomething = (id) => {
this.setState({selectedId: id});
}
}

4. Prefer pure components wherever possible

Pure components adds basic optimisation to component re-rendering condition. So, your component will not be updated if references to props & states remain the same. This might work out perfectly in many use cases — especially if you are using immutable state management in your app. But there also might be places where you might not want this optimisation.

To make your component pure, simply do either of these:

class MyComponent1 extends PureComponent {...}
@PureComponent
class MyComponent2 extends Component {...}

5. Optimise shouldComponentUpdate() wherever possible

Consider a case below:

class ParentComponent extends Component {
render() {
return (
<ChildComponent {...this.props.article} />
)
}
}

class ChildComponent extends Component {
render() {
return (
<SomeComponent
title={this.props.title}
description={this.props.description}
imageUrl={this.props.imageUrl}/>
);
}
}

In this case we’re passing some list of props from parent to child component. However, if we see the code — ChildComponent UI only depends on title, description & imageUrl. So, we don’t want our ChildComponent to re-render if lets say a prop called source changes. In this case, we’re very sure that the component need to re-render only if any prop out of these 3 changes. For cases like this, we can add additional optimisation on shouldComponentUpdate()

shouldComponentUpdate(nextProps, nextState){
return (nextProps.title !== this.props.title || nextProps.description !== this.props.description || nextProps.imageUrl !== this.props.imageUrl)
}

PS: This is a very basic example for the purpose of explanation. Optimising shouldComponentUpdate is typically very useful in components that are part of a list.

6. Use Reselect or Memoize with Redux

Lets consider a typical mapStateToProps in redux:

const mapStateToProps = (state) => {
return {
data: computeData(state.someData, state.someCondition)
}
}
const computeData = (someData, someCondition) {
const data = {};
data.x = process(someData.x, someCondition);
data.y = process(someData.y, someCondition);
return data;
}

You know the problem here? With every pass of redux state — we’re creating a new reference of data. And if this prop is injected to a component that doesn’t have shouldComponentUpdate optimisations to compare different values of data and validate if re-render is required — the component will be re-rendered on every redux pass.

To avoid this — we use something called memoized selectors. Broadly, selectors return value based on some input. Memoized selectors make sure that given same input values — they return the same cached output. The most famous library that does this is reselect.

7. Watch your animations

Writing animations is usually one of the trickiest part where you could make your app slow. Animations are typically of two types —

  • JS based: That does all the frame computation on JS thread and dispatches the final frame to native
  • Purely Native: That offloads all the animation to main thread and requires minimal or no to & fro bridge communication.

Always, prefer using purely native animation. For details I would recommend going here

8. Use pure native navigators if your screen transitions are still janky

Screen transitions are usually the most delicate areas — which can create user’s perception for a slow or fast app. So, this is the area we want to optimise the most.

As a ground rule, for all components that are rendered whenever a screen is presented first — you should always use InteractionManager in componentDidMount() for any performance intensive task .

Also, while picking a navigator library — check if its transition animation happens on native & not JS thread. Almost most good libraries does that.

But if even after making all these optimisations your app seems slow — you should consider switching to pure native navigators. Pure native navigators are the ones that uses native fragment or activities for surfacing individual screens. So, when are you’re moving from one screen to another — its actual activities or fragments transitioning which is typically much faster.

Implementing Libraries: react-native-navigation by Wix, native-navigation by Airbnb

9. Respect the bridge

Since, everything continuously flows in/out of the RN Bridge — its highly essential that we’re mindful of how our actions impact the bridge.

Consider a use-case below:

class MyComponent1 extends Component {

componentDidMount() {
shoot20ApiCallsParallelly();
}

}

Do you see a problem with that? Since, you are making lot of network request parallelly, when the response comes in you might have lot of data coming in via bridge simultaneously which might jank up the bridge and cause UI interactions to slow down.

To avoid this, consider batching up your request and keeping optimal number of parallel request. Also, problems like this happens if you are using Websockets in your React Native app — and moving a lot of data in/out coupled with UI interactions.

To peek on what’s going in/out of the bridge, try using this tool called snoopy in dev mode.

10. Other Hacks

To speed up your overall, there are some other things you should consider:

  • Use FlatList instead of ListView (which is pretty standard as of now)
  • Remove console.log in release apps especially if you’re using libraries like redux-logger that dumps a lot of data on console. Babel plugin transform-remove-console does it automatically for you.
  • Use inline requires to delay loading expensive modules in memory. (Think: lodash)
  • Spend some time profiling your app to dig deep further into what’s causing your app to slow down

Adios!

--

--