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
May 5, 2023Technology
The best CSS trends for 2023

Cascading Style Sheets (CSS) made its debut in 1996 and continue to be an indispensable and evolving component of web development. We will examine the most valuable recent and upcoming characteristics in CSS for this year.

May 12, 2022Technology
Increasing performance by using proper query structure

Earlier in our previous article "Improve efficiency of your SELECT queries" we discussed ways to profile and optimize the performance of SELECT queries. However, to write complex yet efficient SQL queries, there is a thing to remember about.

Jun 27, 2018Technology
How to Work With Legacy Code: Code Refactoring Techniques

In this article we'll review general approach to working with the best kind of projects - the ones with old untested and undocumented spaghetti code and a tight schedule. We'll review anger management techniques, coping mechanisms and some refactoring tips that might come in handy.

May 9, 2018Technology
How to Generate PDF Files in Python with Xhtml2pdf, WeasyPrint or Unoconv

Programmatic generation of PDF files is a frequent task when developing applications that can export reports, bills, or questionnaires. In this article, we will consider three common tools for creating PDFs, including their installation and converting principles.

Sep 22, 2016Technology
Angular Form Validation

In this article, we will describe some useful scripts and directives we use with angular form validation in our projects.

Apr 3, 2011Technology
Sprite cache invalidation

When we use css-sprites it's important to make browser cache them for longest period possible. On other hand, we need to refresh them when they are updated. This is especially visible when all icons are stored in single sprite. When it's outdated - entire site becomes ugly.