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.