Back
Nov 29, 2022

React Performance Testing with Jest

One of the key requirements for modern UI is being performant. No matter how beautiful your app looks and what killer features it offers, it will frustrate your users if it clangs.

React performance optimization is a pretty well-covered topic. If you'd like more information on this,, Fix the slow render before you fix the re-render might be a good starting point. But is there a guarantee these optimizations will last? If not careful, it takes one unstable prop to force the entire app re-render and introduce lags noticeable to the user. Of course, this can be handled by manual testing, but it makes sense to look for some kind of automated solution.

Our experience with Jest

We had this problem at our latest project. This affected a pretty complex application that should be highly responsive to user input. Sometimes an unstable prop would sneak in and force re-render of a large DOM chunk. It was always easy to make it fast again, but we wanted to avoid this in the future. So, we looked for a way to automatically check for changes that affected rendering.

The first step was to understand exactly what metrics we want to catch. Of course, it was tempting to measure something that would matter to the end user. Something like render time would do, but this metric is very fuzzy – so we decided to go with something like the number of renders. This is not ideal, but these tests would be idempotent and independent of their environment. This is also somewhat contrary to the “fix slow renders, not re-renders” philosophy, but it still has a good thing – it helps us catch mistakes without manually re-testing a bunch of cases.

Now for the implementation. We don’t need real DOM for counting re-renders, so we decided to go with Jest. In order to interact with React internals and count re-renders, we used Profiler component. It is basically just a component that accepts children and onRender callback.

image-20221124-154118 (1).png

Technically, this is already enough to count renders. But we wanted to make profiler really easy to use, so we created a Higher-Order Component:

const withProfiler = (Component, id: string = 'rootComponent') => {
  const componentId = `withProfiler${id}`;

  const onRender = () => {
    SnapshotProfiler.__numRenders++;
  };

  const SnapshotProfiler = React.memo((props) => {
    return (
      
        
      
    );
  });

  SnapshotProfiler.__numRenders = 0;

  SnapshotProfiler.clearCounters = function () {
    this.__numRenders = 0;
  };

  return SnapshotProfiler;
};

const Foo = () => (
Foo
) const DecoratedFoo = withProfiler(Foo);

Now we can just wrap any component with this HOC, and we can access the number of times that component has been rendered. Rendering DecoratedFoo will effectively render Foo, but will also store a __numRenders counter. And since it is stored inside the component object, rendering any instance of this component will increase the counter. So, if we render the following jsx:

we will have DecoratedFoo.__numRenders === 2 .

This is all well and good, but it's still a little awkward to have withProfiler HOC to components used in a real app. Fortunately, Jest has flexible tools for mocking, so we could make great use of them.

Let’s say we have a ChessBoard component that renders 64 instances of Square component. On each move it should typically re-render 2 more cells, since a single chess piece moves from square to square. So we can create the following mock setup and write the following tests:

jest.mock("./Square", () => ({
 Square: jest
   .requireActual("../profiler")
   .withProfiler(jest.requireActual("./Square").Square),
}));
describe("Chess performance test", () => {
 function clearCounters() {
     Square.clearCounters();
 }
 beforeEach(() => {
   clearCounters();
 });
 const initialSquareRendersCount = 64;
 test("initial render", () => {
   render();
   expect(Square.__numRenders).toBe(initialCellRendersCount);
 });
 test("piece moving", () => {
   const { rerender } = render();
   clearCounters();
   //
   // here we do some DOM manipulations or push new props to initiate re-render...
   //
   expect(Square.__numRenders).toBe(2);
 });
})

This way we can be sure the chessboard elements are rendered as many times as we need. So if someone accidentally puts a global state in every cell and the entire board will unnecessarily re-render - we will find out.

What we’ve done here is mocked Square, so in tests we have the same component wrapped into withProfiler HOC. Thus, the tests themselves do not alter the component structure, and are rendered just like in a regular environment. The code of the component itself does not change too. So, if we have a complex hierarchy of components and some really specific actions causing too many re-renders of a particular component, we can test just that.

I hope this helps you write more performant web applications. Good luck!

Subscribe for the news and updates

More thoughts
Sep 8, 2023Technology
Smooth React virtual scroll with fixed rows/columns

One of our ongoing projects, Neptyne, introduces an Excel-like grid written in React. We used a library to apply virtual scroll to it, but we stumbled upon a problem with fixed rows and columns inside the grid. Here I would like to describe this problem, how it occurs, and how we handled it.

Apr 27, 2022TechnologyBusiness
How to Choose the Best Javascript Framework: Comparison of the Top Javascript Frameworks

In our article, you will find the best JavaScript framework comparison so that you know for sure how to choose the right one for your project.

Apr 19, 2022Technology
Improve efficiency of your SELECT queries

SQL is a fairly complicated language with a steep learning curve. For a large number of people who make use of SQL, learning to apply it efficiently takes lots of trials and errors. Here are some tips on how you can make your SELECT queries better. The majority of tips should be applicable to any relational database management system, but the terminology and exact namings will be taken from PostgreSQL.

Aug 25, 2017Technology
How to Upload Files With Django

File upload works differently from simple form inputs, which can be somewhat troublesome for beginners. Here I'll show you how to handle uploads with ease.

Jun 14, 2017Technology
How to Deploy a Django Application on Heroku?

In this article I'll show you how to deploy Django with Celery and Postgres to Heroku.

Mar 4, 2011Technology
Css sprite generation

I've created this small sprite to create css sprites. It glues images from directory directory into single file and generates corresponding css.