Fine Tuning Infinite Scroll in React

A real world example of optimizing a common React.js component with the React Profiler and memoization

Fine Tuning Infinite Scroll in React

A common pattern on large media sites is to use a feed to display and peruse the latest content. The page is initially loaded with content up to a certain limit, and then more items are loaded as the user interacts and navigates.

One of the most popular methods of displaying a large amount of content in a seamless way is the "Infinite Scroll" method. We've all seen it, and you've likely implemented it yourself from scratch a few different times. Items are loaded then appended to the feed once the user scrolls within a certain threshold of the bottom of the page. This is exactly how our main feed is implemented on https://barstoolsports.com

Recently we worked on checking some of the performance metrics of the site. This included checking page load times, page load size, etc. Even after making a few adjustments that improved page load speed, there was a noticeable "stuttering" behavior in the infinite scroll that can be observed in the gif below.

Initial jumpy scrolling behavior

Since this behavior is observed long after page load, and we found our api to be super performant, something else was afoot. It must be something inefficient happening in the render cycle.

...React Profiler to the rescue!

The React Profiler allows you to record the render activity of all components on the page within the time frame that you start and stop recording. It then displays components and their ancestors/children in a simple flamegraph. Clicking into any of these components on the flamegraph gives detail about its various render commits.

If you're not familiar with the React Dev tools and more specifically the React Profiler, I recommend reading through the docs here.

I opened the home page locally, opened the dev tools, began profiling, scrolled down to trigger a few data fetching calls for more content, then stopped recording. The following result was the flamegraph of the site, and the number of renders for each static story card in the feed really stood out.

The smoking gun of inefficient rendering

StoryCard in the profiler screenshot above is the first StoryCard component in the Infinite Scroll feed. The various render commits on the right side of the image are describing what point into the profiling session it was rendered (or re-rendered) and how long the rendering took. I noticed that it re-rendered twice for every time the Infinite Scroll was loaded with fresh data appended. First re-rendering while it enters a loading state, and then re-rendering after the new data is appended to the scrolling feed.

Each StoryCard in the InfiniteScroll component was re-rendering whenever InfiniteScroll re-rendered, which is unnecessary because the content of each older card in the feed did not change. How do we avoid excessive rendering for a fairly static view component such as this?

...React.memo to the rescue!

React has many tools in its library for memoization to optimize component performance. One of the most popular in functional React components is the useMemo hook. React.memo works as a similar concept. However; instead of merely memoizing a value within the component as useMemo does, React.memo will memoize the instance of that component. Meaning, it will ensure the component does not re-render until one of the props changes even if a parent component re-renders. Be careful to understand the props comparison rules that dictate whether the component will re-render or not. For more context, refer to the React.memo docs here.

Applying this to our StoryCard component, for example:

const StoryCard = React.memo(({ story, index, feedType }) => {
  ...
})

After re-running and re-profiling, this instantly provided improved results. I looked into the flamegraph and clicked on the InfiniteScroll component and then first StoryCard. The screenshots below show that the InfiniteScroll component loaded data and re-rendered several times as expected, and the first StoryCard component rendered only once as desired.

InfiniteScroll render commit list
StoryCard render commit list

And the result is also noticeable while flying through the infinite scroll on any feed page. The clip below shows a smooth improvement with no stuttering behavior when compared to the earlier clip.

Smooth scrolling

This is a fairly specific example that focuses on optimizing an infinite scroll component in particular. However, it's important to realize that these same tools/approaches can be used to optimize any view components that have fairly static content. If you're noticing bogged down or jumpy behavior in your React site, it could be caused by inefficient rendering. Hunt down the culprit with the React Profiler, refactor, and apply memoization optimizations where applicable to prevent the over-rendering that causes any undesired behavior.