Pavel Vlasov

Pavel Vlasov
Feb 23, 2018

Redux ruins your React app performance? You are doing something wrong

Redux ruins your React app performance? You are doing something wrong

I often hear people saying Redux is slow and can be a root cause of unnecessary re-rendering. In this article I’ll try to explain how to avoid common mistakes when working with react-redux binding.

Assuming you already know about basic techniques to avoid reconciliation, otherwise you can read about it in official react guide.

There’re lots of common techniques, like using PureComponent, reselect and memoization, however when it comes to a complex application, even following all the rules still opens the gap for performance problems.

Defining root cause

Using react perf tools it’s quite easy to see what part of the application gets re-rendered. But the main question is why it gets re-rendered. To quickly identify that you can use the following snippet:

import { PureComponent } from 'react'
PureComponent.componentDidUpdate = prevProps => {
    const name =
        this.constructor.displayName || this.constructor.name || 'Component'
    console.group(name)
    Object.keys(prevProps).forEach(key => {
        if (prevProps[key] !== this.props[key]) {
            console.log(
                `property ${key} changed from ${prevProps[key]} to ${
                    this.props[key]
                }`
            )
        }
    })
    console.groupEnd(name)
}

It will pretty print properties, that caused your component to re-render. Try to put this snippet into root module of your application and then dispatch redux action, that does not exists in your system. I bet you’ll find lots of interesting things.

Common pitfalls

I’ll try to list most common mistakes breaking equality check and force your components to always re-render.

Selectors without memoization.

Most obvious one. Selectors like

const getList = (state, filter) => state.list.filter(filter)
const getConfig = state => {
    return { foo: state.foo }
}

will always cause re-render. Try to use reselect or another memoization technique to fix it.

Edge cases handling

Selectors that return default value if it does not exists also can cause performance regression:

const getList = state => state.list || []

On every redux store changes it will return new instance of array, but you can easily fix it by defining default constant:

const defaultList = []
const getList = state => state.list || defaultList

Logic in connected component

import {connect} from 'react-redux';
...
export default connect((state) => ({
    filter: {foo: state.filter}
}))(MyComponent);

Such logic should be moved into selector and properly memoized.

React elements in props

import {connect} from 'react-redux';
...
export default connect((state) => ({
    element: <List />
}));

element will always be a new instance, so it also breaks equality check. But it’s pretty easy to fix by moving it to a constant, defined in the module:

import {connect} from 'react-redux';
...
const element = <List />;
export default connect((state) => ({
    element
}));

Another way is to pass component instead of instance and delegate rendering to a parent:

import {connect} from 'react-redux';
...
export default connect((state) => ({
    Element: List
}));
...
{
    render() {
        const {Element} = this.props;
        return (<div>{<Element />}</div>);
    }
}

Dynamic handlers

class MyComponent extends PureComponent {
    render() {
        const { title, onClick } = this.props

        return <InnerComponent title={title} onClick={() => onClick()} />
    }
}

InnerComponent will always be re-rendered because it always get’s new instance of onClick handler. You can simply fix it by moving handler to a component class property:

class MyComponent extends PureComponent {
    onClick = () => {
        this.props.onClick()
    }

    render() {
        const { title, onClick } = this.props

        return (
            <InnerComponent onClick={this.onClick}>
                <span>{title}</span>
            </InnerComponent>
        )
    }
}

Tips and tricks

Now let’s elaborate on how you can make it easier to avoid such mistakes in future.

Make components smaller

It’s quite bad idea of subscribing a container component to a bunch of state and then passing it down via props, for example:

class MyBeastComponent extends PureComponent {
    render() {
        const { a, b, c } = this.props

        return (
            <div>
                <ComponentA a={a} />
                <ComponentB b={b} />
                <ComponentC c={c} />
            </div>
        )
    }
}

Every time something changes in the a, b or c it will cause all the chunk to re-render. Instead you can split it into smaller connected components:

class MyBeastComponent extends PureComponent {
    render() {
        return (
            <div>
                <ComponentA />
                <ComponentB />
                <ComponentC />
            </div>
        );
    }
}
... ComponentA, ComponentB or ComponentC
import {connect} from 'react-redux';
export default connect(state => ({
    a: getA(state)
}))(ComponentA);

Reduce data scope

Pass only data that your component needs. For example, to render collection, usually instead of passing the whole list you can just pass ids of items. Then connect children to the store and use id to retrieve the data:

class MyList extends PureComponent {
    render() {
        const {itemIds} = this.props;

        return (
            <div>
                {itemIds.map(id => (
                    <Item key={id} id={id} />
                ))}
            </div>
        );
    }
}
... Item component
import {connect} from 'react-redux';
export default connect((state, {id}) => ({
    title: getItemTitle(state, id),
    foo: getItemFoo(state, id),
    bar: getItemBar(state, id)
}))(Item);

In this case, even if set of ids in your collection changes, it won’t re-render all the list including child items.

Double check yourself

Always make sure your memoization works and you not producing any regressions. You can use following snippet to wrap you selectors in unit tests and double check it returns the same result for the same set of arguments:

const wrapMemoizedSelector = (selector) => {
    returns (...args) => {
        const result = selector(...args);
        if (selector(...args) !== result) {
            throw new Error('Memoization check failed.');
        }
        return result;
    }
}
...
const examine = wrapMemoizedSelector(selector);
test('check return value', () => {
    expect(examine({...})).toEqual({...});
});

That’s it!

Hope that will help make you react apps rapid fast.

The article was originally posted on medium.