Fixing Slow React Renders

A recent project at work tasked me with improving the index page of our web app to show tens to hundreds of a Hexagon component, with an associated tooltip that would appear when hovered over a particular hexagon.

The hexagon is simple enough - just a path inside of a larger SVG, and the tooltip shows some additional information about the underlying resource the hexagon represents.

function Hexagon() {
	return (
		<path d="M 25 3
                 q  5 -3,  10  0  l  21  12
                 q  4  3,   4  8  l   0  25
                 q  0  4,  -4  7  l -21  12
                 q -5  3, -10  0  l -21 -12
                 q -4 -3,  -4 -7  l   0 -25
                 q  0 -5,   4 -8  Z" />
	);
}

The container component contained the hover logic from the original implementation.

class Container extends React.Component {
    state = {
        hovered: null,
    }

    onHover = (hovered) => {
        this.setState({ hovered })
    }

    render() {
        const { data } = this.props

        return (
            {data.map(d => (
                <HexagonContainer
                    data={d}
                    hovered={d.id === hovered.id}
                    onHover={() => this.onHover(d)}
                    onHoverLeave={() => this.onHover(null)}
                />
            ))}
        )
    }
}

function HexagonContainer({ hovered }) {
    return (
        <a
            href={...}
            onBlur={onHoverLeave}
            onFocus={onHover}
            onMouseOut={onHoverLeave}
            onMouseOver={onHover}
        >
            <Hexagon active={hovered} />
        </a>
    )
}

This worked fine when there were only expected to be a maximum of five containers, but was incredibly slow when this implementation was scaled to around 80 onscreen at once, and the tooltip would lag far behind the mouse moving across the page, sometimes taking a few seconds to render at the correct position once the mouse had stopped moving.

Any React experts reading this can probably spot the issue already, but it took me a little while of tracking down some other possible issues (changing some components to use React.memo() or PureComponent) before realizing the cause of the major slowdown.

So what is the issue?

The real problem was an excessive number of rerenders due to new onHover and onHoverLeave functions getting created and passed down to the HexagonContainer component every time the user moved their mouse. The Container component had to rerender, because the hovered state would update on mouse movement. This caused 100+ HexagonContainer components to get two new functions created for them, and since those functions were different, React determined that they needed to rerender as well.

Thanks to the profiler in the React devtools, as well as diffing prevProps and props inside componentDidUpdate, I was able to determine the functions as the cause of the rerenders.

But how to fix it?

To avoid rerendering unnecessarily, the functions should only be recreated when their data actually changes. My initial thought was to update HexagonContainer to use React.memo, so that it would not rerender on a shallow update, but that wasn't a viable solution because the functions did have to be different (as they set a different hexagon to get hovered).

So, in the end I simply moved the function creation down another layer:

class Container extends React.Component {
    state = {
        hovered: null,
    }

    onHover = (hovered) => {
        this.setState({ hovered })
    }

    render() {
        return (
            {data.map(d => (
                <HexagonContainer
                    data={d}
                    hovered={d.id === hovered.id}
                    onHover={this.onHover}
                />
            ))}
        )
    }
}

function HexagonContainer({ hovered }) {
    return (
        <a
            href={...}
            onBlur={() => onHover(null)}
            onFocus={() => onHover(data)}
            onMouseEnter={() => onHover(data)}
            onMouseLeave={() => onHover(null)}
        >
            <Hexagon active={hovered} />
        </a>
    )
}

export React.memo(HexagonContainer)

I also wrapped HexagonContainer with a React.memo call as well. Now, the onHover function passed to HexagonContainer will only change if Container rerenders, which in this case only happens if the data prop changes.

There were some other additional tweaks I made as well, but this was the major improvement that fixed the lagging tooltip issue.