Performance Tunning in ReactJS

Versão em português

You are working in a ReactJs project, codebase looks okay and everything is fine. But for some weird reason, the application presents slowness, and just the bundle optimization plugins aren't being enough. Wath did you do?

Hello World! I'm Gabriel Carvalho

FrontEnd developer since 2019.
Passionate for technology and what I do.

Github LinkedIn Instagram

Why optimizing?

Lets begin with a questioning. After all the app is already in production. My work is done, now it's just maintenance right? Well, I hope that you don't get to the point of mandatory review your app performance. But if it's you case, or do you simply is interested in improving a bit of your user's experience, that article may help.

Rendering

Do you know how React renders its components?

Whenever the props(that arguments passed by father components), or the components own state changes, React rebuild that fella, AND all its tree.

That is, if you have a tree of 3 nodes and the first one re-render, its child, and the next descendant are submitted to the rendering circle.

In an giant application that it's components and hooks are not well planned, that can turn into a mess of circle of rendering and data flowing through any direction really really fast.

How do we put out the fire?

We're going to use React DevTools to identify leaks in the rendering process.

We have a simple app

const App: FC = () => (
  <PokeListProvider>
    <FilterProvider>
      <main className="App">
        <header className="App-header">
          <h1>
            POCKEMÓNS
          </h1>
        </header>

        <Filter />

        <PokeList />
      </main>
    </FilterProvider>
  </PokeListProvider>
);

export default App;
const PokeList: FC = () => {
  const { loadingRowList, pokemons } = usePokeList();
  const { pokemonsToShow } = useFilter();
  
  return (
    <section className='pokelist'>
      {loadingRowList ? (
        <p className='loading'>Carregando Pokemons</p>
      ) : pokemons.map((pokemon) => pokemonsToShow.includes(pokemon.name) && (
        <PokeCard
          key={pokemon.name}
          pokemon={pokemon}
        />
      ))}
    </section>
  );
}

export default PokeList;
const Filter: FC = () => {
  const { form, onSubmit } = useFilter();

  return (
    <form onSubmit={onSubmit} className="filter-form">
      <input placeholder='Nome' {...form.register('name')} />
      <input placeholder='Tipo' {...form.register('type')} />
      <button type='submit'> Buscar</button>
    </form>
  );
}

export default Filter;

Notice that the initial rendering loads all our cards

When I search for grass pokemons

Did you realised?

Even that bubassour's card hasn't came out the screen, it renders again, AND ALL GRASS POKÉMONS TOO

The use of React.memo

The “memo” is a Higher Order Component, wich tells React that that component passed to it, must be memoized.

That way React does a little initial effort to memoize the component, avoiding future unnecessary rebuild.

export default memo(PokeCard);

Note that now we haven't any new render

When I remove the filter, only the cards that wasn't in the screen suffer changes

20,5 ms

Before memo

2,9 ms

After memo

So, it's just using memo everywhere

Right?

Wrong

We must be very careful using memo, because it requires more processing from the client's machine to memoise components.

Beside that is very useful when our component is rebuild without any props changing, it isn't quite effective in components that changes a lot, as PokeList doe so. A memo there would only turn our app heavy unnecessarily.

And when we have tons of components in our application, we must analyze wizely before got memorizing everything.

Okay, but what about useMemo?

We may use the "useMemo" to memorize heavy processing and event lists of components. Taking the same careful, because useMemo also requires processing to do this memoize.

In case of our <PokeList /> had another elements that trigger rendering, whit no need to affect our pokemons, we could use useMemo to memorize our cards, “by passing” the <PokeList />'s rendering, triggered by other stuff(props, hooks, etc).

const PokeList: FC = () => {
  const { loadingRowList, pokemons } = usePokeList();
  const { pokemonsToShow } = useFilter();

  const cardList = useMemo(() => pokemons
    .map((pokemon) => pokemonsToShow.includes(pokemon.name) && (
      <PokeCard
        key={pokemon.name}
        pokemon={pokemon}
      />
    )), [pokemons, pokemonsToShow]);

  return (
    <section className='pokelist'>
      {loadingRowList ? (
        <p className='loading'>Carregando Pokemons</p>
      ) : cardList}
    </section>
  );
}

Custom memo

It is possible too pass a function that validate the props and return a boolean in reason that React either rebuild or not our component.

export default memo(PokeCard, (
  prevProps: Readonly<PokeCardProps>,
  nextProps: Readonly<PokeCardProps>,
) => {
  return JSON.stringify(prevProps.pokemon) !== JSON.stringify(nextProps.pokemon)
});

Infinite scroll

Another technique we may use is the “infinite scroll”.

It consist into basically limit the amount of showing items in screen, and load more only when needed.

In other words, the user get near to the page end, new items are feed to him.

Implementation Example

const [scrollTimeout, setScrollTimeout] = useState(null as unknown as NodeJS.Timeout);

  const onScroll = () => {
    const scrolledToBottom =
      window?.innerHeight + Math.ceil(window?.pageYOffset) + 300 >=
      document?.body.offsetHeight;

    const haveMoreToShow = amountToShow < (pokemonsToShow?.length);

    if (scrolledToBottom && haveMoreToShow) {
      if (scrollTimeout) clearTimeout(scrollTimeout);

      setScrollTimeout(
        setTimeout(() => {
          setAmountToShow(amountToShow + 10);
        }, 50),
      );
    }
  };
useEffect(() => {
    window?.addEventListener('scroll', onScroll);
    return () => window?.removeEventListener('scroll', onScroll);
}, [onScroll]);
const { loadingRowList, pokemons } = usePokeList();
  const { pokemonsToShow, amountToShow } = useFilter();

  const cardList = useMemo(() => pokemons
    .slice(0, amountToShow)
    .map((pokemon) => pokemonsToShow.includes(pokemon.name) && (
      <PokeCard
        key={pokemon.name}
        pokemon={pokemon}
      />
    )), [pokemons, pokemonsToShow, amountToShow]);

Bônus

We briefly discussed about memorizing here. But there are many more topics that may be interesting to you

Try too

  • React.lazy and code splitting
  • WebWorkers
  • Data flow management libraries (Redux, MobX, BLoC)

Thank You!

Any doubt? You can find me in:

Github LinkedIn Instagram

Credits

Special thanks to Mariana, for putting up with me talking about code for hours and being my proofreader

Powerpoint Presentation(Portuguese)

Link to the project

não posta em inglês não cara...aqui o canal é todo em português..

Foi só para ter documentado em inglês também, no todo tem o link para a versão em pt. Eu não sabia que conteudos em outras linguas não eram bem vindos ao tabnews. Acho melhor eu criar uma conta no medium. Conhecimento não deve ser restrito a idiomas.

Pretty good article