Copied to clipboard
series
Series
Insights

Fixing common performance problems in React Navigation

An exciting project in React Native for one of Belgium’s finest soccer teams turned into an opportunity to fix some React Navigation performance issues. Let us show you how we did it.

17/7/2023
6
min read

Recently, we launched the first version of the app we are building for RSC Anderlecht, one of Belgium’s leading soccer teams. The app – dubbed ‘RSCA Fan Engagement Platform’ – lets fans access exclusive content, engage in a live feed of every match, take pictures with custom filters and much more.

We set out to build the app in React Native for both Android and iOS. In this blog post, we’d like to focus on the navigation in the RSCA app, which gave us a headache at times.

First, a look at the current state of React Native

React Native has long been on our radar. We’ve used it in some of our products and found it had great potential. But we’d been holding back from adopting it in bigger projects, because we felt this ‘young’ technology first needed to prove itself at scale. Then, in the past year, a lot of things changed in the React Native realm. React Native and many third-party libraries have become more stable, and new libraries are being created every day.

The perfect moment to get our first major project in React Native on the road.


Now let's cut to the chase... navigation!

For the RSCA app, we decided to use the React Navigation library. We won’t go into detail as to why we chose this library from the herd of navigation solutions, but here’s a great post written on this topic!

We started by running a few tests with React Navigation: adding the navigation state to Redux, pushing new screens onto a stack, handling deeplinks, etc. It all worked without a hitch! So we cleaned up the tests and added the rest of the screens.

This is when things started to go a little less smoothly.. On some screens – specifically those with lots of components – we started noticing a few things…

Right off the bat, there is a substantial delay between the user pressing a button and the swipe-in animation of a new screen. When a new screen is pushed, React Navigation initially renders it off-screen and animates it into place afterwards. This means that when a complex screen is pushed, e.g. a screen with lots of components that easily takes a few hundred milliseconds to render, it feels less snappy than a natively written application. There can also be some undesirable side effects: for instance, if you tap a button quickly a few times, the same route can be triggered multiple times.

Another problem is that business logic can be executed while the swipe-in animation is doing its thing. This can make for a not smooth animation. The issue is caused by one of the major downsides of React Navigation, and React Native in general: JS-accelerated animations and business logic run on the same thread, the JavaScript thread. In fact, React Native only has one thread where all work needs to be done. So when complex JS-accelerated animations are executed in combination with business logic, the JS-thread drops frames, causing a jerky animation.

Of course, we only want perfect, buttery smooth animations! So how did we work with these shortcomings?

In our RSCA app, most of the screens that are pushed onto the stack need to fetch data, meaning that actions are dispatched to the store and the corresponding saga starts an API-call. To solve our issues with navigation and animations, we needed to find a way to delay the fetching of new data until after the swipe-in animation completes. It turns out that React Native has an API for checking animations. InteractionManager provides a handy callback that fires after all animations are done, allowing us to only start fetching new data after our swipe-in animation. Let’s take a look at how this would be implemented:

{% c-block language="js" %}
/* // @flow */

import { InteractionManager } from 'react-native';

type Props = {
dispatchTeamFetchStart: Function,
};

class Team extends Component<Props> {
/* // Lifecycle methods */

componentDidMount() {
/*   // 1: Component is mounted off-screen */
  InteractionManager.runAfterInteractions(() => {
/*     // 2: Component is done animating */
/*     // 3: Start fetching the team */
    this.props.dispatchTeamFetchStart();
  });
}

/* // Render */

render() {
/*   //  Render the navigation bar and a list of players */
  ...
}
}


export default Team;
{% c-block-end %}



With this fix, the animation doesn’t jerk anymore – wonderful!


Well, almost.

Unfortunately, fixing this issue causes deeplinks to break – not so wonderful.

When handling a deeplink, the screen displays without an animation. With no animations to handle, the runAfterInteractions callback is not called when the component mounts. Fortunately, this can be easily fixed.


The next step is to reduce the duration of the initial off-screen render. We can do this by leveraging the same callback InteractionManager provides. On the initial render, we try to show as little data as possible to make sure the swipe-in animation starts right away.


Let’s take a look at the team list for example:

We start by loading the navigation bar and activity indicator. When the animation-end callback on InteractionManager is triggered, we update the view to show cached data, and dispatch a fetch data action to the store. So now, when we tap a button, the animation starts immediately – without delays!

Let’s take the previous code example and extend it…

{% c-block language="js" %}
/* // @flow */

import React, { Component } from 'react';
import { InteractionManager } from 'react-native';

type State = {|
didFinishInitialAnimation: boolean,
|};

type Props = {
dispatchTeamFetchStart: Function,
};

class Team extends Component<Props, State> {
constructor(props: Props) {
  super(props);

/*   // 1: set didFinishInitialAnimation to false */
/*   // This will render only the navigation bar and activity indicator */
  this.state = {
    didFinishInitialAnimation: false,
  };
}

/* // Lifecycle methods */

componentDidMount() {
/*   // 1: Component is mounted off-screen */
  InteractionManager.runAfterInteractions(() => {
/*     // 2: Component is done animating
    // 3: Start fetching the team */
    this.props.dispatchTeamFetchStart();
   
/*     // 4: set didFinishInitialAnimation to false
    // This will render the navigation bar and a list of players */
    this.setState({
      didFinishInitialAnimation: true,
    });
  });
}

/* // Render */
render() {
/*   // When didFinishInitialAnimation is false
  //  - render just the navigation bar and activity indicator
  // When didFinishInitialAnimation is true
  //  - render the navigation bar and a list of players */
  ...
}
}

export default Team;
{% c-block-end %}


With all that done, there’s one more issue to fix: it’s still possible to trigger the same route multiple times by quickly tapping a button more than once. Luckily for us, the React Native community has already found a solution for this. By adding a so-called debouncer to our Redux Middleware, we can cancel actions that have the same route if they are triggered multiple times within a specified timeframe.

Thomas Van Sundert

Co-founder & Director Engineering

TO CONCLUDE…

By introducing these optimisations we notably improved the navigation’s animations. Of course, you don’t have to add these optimisations to every screen in your app. What we did was go through the app, pinpoint inefficiencies and optimise the screens that really needed it.


So go ahead and give it a try!

Thomas Van Sundert

Co-founder & Director Engineering

17/7/2023
6
min read

We help companies
succeed in the digital age

Stay up-to-date with November Five

Follow us on LinkedIn for insights, learnings, use cases and more.

Looking for a partner that thinks beyond delivery?

We help companies
succeed in the digital age

Let’s get to know each other better and explore how we can help your business embark on a journey towards digitally enabled success.

CONTACT US
Series
DISCOVER MORE INSIGHTS
Chevron
About Fast Company’s ‘Best Workplace for Innovators’

November Five was named one of Fast Company’s global 100 Best Workplaces for Innovators in both 2020 and 2021. This annual list, developed in collaboration with Accenture, recognises and honors the top 100 businesses from different industries that inspire, support and promote innovation at all levels. For the consecutive year, November Five was the single Belgian workplace listed.

Fast Company is the world's leading progressive business media brand, with a unique editorial focus on innovation in technology, ethical economics, leadership, and design. Written for, by, and about the most progressive business leaders, Fast Company and FastCompany.com inspire readers and users to think beyond traditional boundaries, lead conversations and create the future of business.

Jeroen Van Winckel

Product Strategy Designer

Ralph Van Tongelen

Finance Director

Office

Office

Dario Prskalo

Associate to the executive team

Brecht Spileers

Chief of Staff & Director Corporate Strategy

Emily Stewart

Senior Content Writer

Rindert Dalstra

Brand & Marketing Director

Robin Van den Bergh

Managing Director at Appmiral

Maarten Raemdonck

Co-founder & Managing Director at Spencer

Phillip Vandervoort

Executive advisor - Strategy

Vincent Bruyneel

CFO & COO

David Du Pré

Executive advisor

Marc Wojciechowski

Assistant Director

Muriel Mwema

Director Product Management & Delivery

Nick Verbaendert

Co-Founder & Director Business Operations

David De Bels

Product Owner at Appmiral

Tom Vroemans

Co-founder & CEO

Veronique Verhees

Talent Manager

Jens Reynders

Engineer

Michiel Van Nueten

UI Designer

Samuel De Pooter

Engineer

Bert Hofmans

UI Designer

Stijn Symons

Director Architecture

Vincent Pauwels

Co-founder & Director Experience Design

Thomas Van Sundert

Co-founder & Director Engineering

Justin Mol

Director Client Partnerships

Leslie De Cuyper

Client Partner

Ruben Van den Bossche

Chief Operating Officer

Nikki Jacobs

Managing Director at The Market