Performance pitfalls for React applications and how to find them

BlaBlaCar offers an international transportation service, which reaches to a large audience on web and mobile web. Audience navigating on any type of device and any network, from clunky 3G on a 3 years old cheap smartphone, to fast Wi-Fi connection on the latest iPhone.

Today, users are expecting the web to be fast. A study by Soasta shows that a fast experience decreases your bounce rate and results in longer sessions. For more information see Google: 53% of mobile users abandon sites that take longer than 3 seconds to load.

Your app’s performance is a known key factor to improve user retention and return rate — which is something your business probably wants as well.

Detecting performance issues, fixing them, and monitoring our new React app behavior is our job at BlaBlaCar as Frontend developers.

Delivering things faster

First meaningful paint and first interactive are two important concepts when talking about loading issues. The former is the moment where your first content is displayed to your user, and the latter is the moment when the user can start interacting with your page. Both need to come as early as possible for a better experience.

What do you mean by “This page took 15 seconds to load”??

Keep track of what’s in your code

One of the most obvious way to improve your load time is to reduce the global weight of your page. Lighter assets will obviously load faster, but it’s not always easy to know which parts of your script affect its weight the most.

At BlaBlaCar, we use Webpack 3 to bundle our scripts, and it comes with a wide variety of tools to analyze what’s in your bundles. All you have to do is generate a stats file when you build your project with this command:

webpack --profile --json > stats.json

You can use this file with different tools like webpack-bundle-analyzer and clearly see whhich scripts and libraries are the heaviest. I recommend doing this often, especially when adding external libraries to your project.

Know what you bring along

Split your application in smaller parts

If some parts of your application do not need to be available instantly, consider putting the necessary scripts in a separate bundle and load it later when needed. Webpack provides an easy way to do this through the dynamic import() function.

class MyComponent extends React.Component {
state = {
Bar: null
}

componentWillMount() {
import('components/Bar').then((Bar) => {
this.setState({ Bar })
})
}

render() {
const { Bar } = this.state
if (!Bar) {
return <div>Loading...</div>
} else {
return <Bar/>
}
}
}

At BlaBlaCar, we use React Loadable to easily create smaller bundles for the different parts of our single page app. This helps a lot reducing the main bundle size and only load what is necessary on the first render. It also provides a nice API to handle loading state and errors.

Fetch your scripts magically when needed

Put the server in charge with server-side rendering

With web apps built in JavaScript, until your script is fully loaded and executed, nothing is displayed to your user except a blank page. This slows down your first meaningful paint, and also has a negative impact on SEO as your page contains nothing before JavaScript is being executed.

Isomorphic React apps solve this issue by having the same code run on both server and client. On first render, the server will fetch information, render your React tree and return it as HTML code in the response. Then, the client will re-sync with what has been delivered by the server and handle the next user actions itself.

SSR is not mandatory for all apps, and can be completely ignored if you don’t have any need for SEO, or if you don’t have access to really good servers — or Node JS servers at all. It also complexifies your app a lot, so unless you have a clear need for it, skipping it will usually be the best solution. For more information on when to use SSR, see Should I use React Server-Side Rendering? by JavaScript Stuff.

There are also other minimalist solutions for SEO, like prerender, which can be enough depending on your app’s purpose and structure.

Let your server handle some of the job

Keep your user experience smooth and fast

Users expect your interface to answer to any interaction in a fraction of second, and their navigation to be smooth. If the first load matters a lot, so is your app global perceived performance.

Jagged animations, poor scrolling speed, or wait time when clicking on a button can frustrate users, and result in a negative perception of your application.

Using Chrome DevTools to identify slow components

If you are using React 16 and Google Chrome, your life has been made easier, since React now sends small events to Chrome DevTools when executing updates or renders, which can help identify methods that take a lot of CPU.

In dev mode, open your Performance tab in your console and start a new profile.

Example: our datepicker takes a lot of time to mount, this needs a fix!

When analyzing your profile, try to focus on parts marked by a red bar indicator, as this is a marker for heavy CPU tasks. The User Timing accordion shows all the logs sent by React on your components methods. This is extremely useful to identify which parts of your app take a very long time to update or render.

Learn more on how to use your developers tools with Google Chrome Devtools official documentation.

Use all the handy tools at your disposal

Speed up update and rendering with functional components

Functional components are stateless and only implement a rendering method. They are declared as pure functions, and not classes.

Those types of components are way faster to update and render, since they do not implement any lifecycle method, nor carry any state or this context. If some of your components do not need any of these, you should declare them as functional components.

const MyComponent = (props) => <div>{props.foo}</div>

Choose when to update your React tree

A major performance pitfall for React apps are non-desired tree updates. A prop change at the top of the tree will update all children components — and their children’s children, etc… — which can take a long execution time, depending on the size and complexity of your React tree.

You can interrupt the whole tree update by implementing shouldComponentUpdate lifecycle methods where needed. Keep in mind that this will prevent all your component’s descendants from updating, not just only its direct children.

Another way to prevent this is to implement components using React.PureComponent: pure components are similar to regular React.Component, except that they implement their own shouldComponentUpdate method, which does a shallow comparison of states and props before updating the component. This means that a pure component will not update unless its direct props, or state, have changed, which can result in performance gains when applied to very complex components.

Make sure it only changes when you want it to

Check for jagged animations and scrolling

FPS (frames per second) drops during animations and scrolling can be imputed to non-desired script executions in most cases. Using Chrome Dev Tools and React Dev Tools, you can easily identify which components are being updated.

In any case, while animating components — or while scrolling — you should avoid updating the local state of your component or global state of your app repeatedly. If you need to update information, consider debouncing your updates, or store them in your local context instead of your state to avoid unnecessary re-render.

Going further

Checking performance issues is a task that you need to perform regularly while developing a new application, and knowing how to avoid common issues will help you deliver better quality.

I’ll always recommend doing a quick tour of your app behavior and speed when developing new features or performing core updates, as a simple change can have unmeasurable repercussions. Try to automate some of these tasks: bundle sizing can easily be checked with a custom script as a hook. There are also monitoring tools, like Speedcurve, that you can incorporate in your CI and whole release process, and quickly detect regressions before delivering.

It’s always better when it’s automated

For this time, I mainly focused on JavaScript optimization and performance, but there are a lot of other sources for performance pitfalls in a React app. I’d like to go deeper with some of them, but this would take a whole new article, and I’m out of time!

So stay tuned for our next magical stories!