react-performance-testing
This library is perfect for testing React or ReactNative runtime performance. react-performance-testing
counts the number of renders and the render time in a test environment.
Table of Contents
The problem
If you need to develop high-performance features, you need to count renders and render time. Normally you would have to go through the arduous process of manually checking dev-tools or Lighthouse. With react-performance-testing
you can automate this process, saving you time and ensuring you always have one eye on performance.
The solution
react-performance-testing
monkey patches React
to provide you with an API that can count the number of renders and measure render time.
Installation
npm:
npm install --save-dev react-performance-testing
yarn:
yarn add --dev react-performance-testing
Use jest-performance-testing for a great testing experience.
Additionally, you can use performance-testing-cli if you use renderTime
. If you use this library, you can execute test for each files. Therefore you will not need to test one by one.
If you want to use jsx-runtime
in [email protected], you need to modify tsconfig.json
like the following.
"compilerOptions": {
"jsx": "react-jsxdev",
"jsxImportSource": "react-performance-testing"
}
Example
count renders
test('should have two renders when state is updated', async () => {
const Counter = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<p>{count}</p>
<button type="button" onClick={() => setCount((c) => c + 1)}>
count
</button>
</div>
);
};
const Component = () => {
return <Counter />;
};
const { renderCount } = perf(React);
render(<Component />);
fireEvent.click(screen.getByRole('button', { name: /count/i }));
await wait(() => expect(renderCount.current.Counter.value).toBe(2));
});
test('should have two renders when state is updated with multiple of the same component', async () => {
const Counter = ({ testid }) => {
const [count, setCount] = React.useState(0);
return (
<div>
<p>{count}</p>
<button
data-testid={testid}
type="button"
onClick={() => setCount((c) => c + 1)}
>
count
</button>
</div>
);
};
const Component = () => {
return (
<div>
<Counter />
<Counter testid="button" />
<Counter />
</div>
);
};
const { renderCount } = perf(React);
render(<Component />);
fireEvent.click(screen.getByTestId('button'));
await wait(() => {
expect(renderCount.current.Counter[0].value).toBe(1);
expect(renderCount.current.Counter[1].value).toBe(2);
expect(renderCount.current.Counter[2].value).toBe(1);
});
});
measure render time
If you want to use renderTime
, please check out the docs: renderTime.
test('render time should be less than 16ms', async () => {
const Counter = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<p>{count}</p>
<button type="button" onClick={() => setCount((c) => c + 1)}>
count
</button>
</div>
);
};
const { renderTime } = perf(React);
render(<Counter />);
fireEvent.click(screen.getByRole('button', { name: /count/i }));
await wait(() => {
// 16ms is meaning it is 60fps
expect(renderTime.current.Counter.mount).toBeLessThan(16);
// renderTime.current.Counter.updates[0] is second render
expect(renderTime.current.Counter.updates[0]).toBeLessThan(16);
});
});
test('should measure re-render time when state is updated with multiple of the same component', async () => {
const Counter = ({ testid }) => {
const [count, setCount] = React.useState(0);
return (
<div>
<p>{count}</p>
<button
data-testid={testid}
type="button"
onClick={() => setCount((c) => c + 1)}
>
count
</button>
</div>
);
};
const Component = () => {
return (
<div>
<Counter />
<Counter testid="button" />
<Counter />
</div>
);
};
const { renderTime } = perf(React);
render(<Component />);
fireEvent.click(screen.getByTestId('button'));
await wait(() => {
expect(renderTime.current.Counter[0].updates).toHaveLength(0);
expect(renderTime.current.Counter[1].updates[0]).toBeLessThan(16);
expect(renderTime.current.Counter[2].updates).toHaveLength(0);
});
});
API
If you use the API with a large component, the component's performance could be affected because we monkey patch React.
Therefore, if you want to measure accurately, you should use the API with components that have one feature like List
, Modal
etc.
perf
perf
method observes your component. So you can get the renderCount
to count the number of renders and renderTime
to measure render time.
const { renderCount, renderTime } = perf(React);
Note that You need to invoke the perf
method before the render
method is invoked. Additionally, You need to pass React
to the perf
method because we are monkey patching React
.
Note: If you are using jsx-runtime
in [email protected], you don't need to pass React
for monkey patching.
Note: You need to wrap the returned value with wait method.
renderCount
renderCount
will count the number of renders.
const Component = () => <p>test</p>;
const { renderCount } = perf(React);
// render is imported from react-testing-library
render(<Component />);
wait(() => console.log(renderCount.current.Component.value)); // output: 1
Note: You need to set a display name. If you have an anonymous component, we can not set the renderCount
property correctly.
Properties
renderCount.current
ComponentName: string | Array
value: number
Note: If you have the same component, these components combine into an array
renderTime
renderTime
will count the time elapsed between renders.
const Component = () => <p>test</p>;
const { renderTime } = perf(React);
// render is imported from react-testing-library
render(<Component />);
wait(() => {
console.log(renderTime.current.Component.mount); // output: ...ms
console.log(renderTime.current.Component.updates); // output: []
});
Note: If you want to measure render time, you need to test renders one by one. V8 has a feature called inline caching, so if you measure just the result there will be a large difference. Therefore You need to execute tests one by one like jest --testNamePattern=...
or jest src/something.test.js
.
Note: You need to set a display name. If you have an anonymous component, we can not set a property to renderTime
correctly.
Properties
renderCount.current
ComponentName: string | Array
mount: number
... This property has the initial render time.updates: Array<number>
... This property has the second and the subsequent render time (the second render is the index of0
)
Note: If you have the duplicate components, these components combine into array
Note: time is measured in ms
, milliseconds elapsed.
wait
The wait
method is a feature that waits for renderCount
or renderTime
to be assigned. We need to wrap all returned values from perf()
because we are assigning renderCount
and renderTime
asynchronous. If we were to assign some value to renderCount
or renderTime
synchronous, extra processing would be included in the rendering phase.
wait(() => console.log(renderTime.current.Component));
cleanup
The cleanup
method is executed automatically in afterEach()
if you are using Jest
, Mocha
, and Jasmine
. You need to clean up your component by using cleanup
.
If your testing library has afterEach()
, you need to invoke cleanup()
manually.
ReactNative
If you are using ReactNative, you need to import modules from react-performance-testing/native
.
import { perf } from 'react-performance-testing/native';
TypeScript
If you are using Typescript, you can get benefits from type inference as seen below.
const Text = (): React.ReactElement => <p>test</p>;
const Component = (): React.ReactElement => (
<div>
<Text />
<Text />
</div>
);
// If you didn't pass your type to the type argument
const { renderCount, renderTime } = perf(React);
renderCount.current // Editor will suggest `Text | Text[]` and `Component | Component[]`
// If you passed your type to the type argument
const { renderCount, renderTime } = perf <{ Text: unknown[], Component: unknown }> React;
renderCount.current // Editor will suggest `Text[]` and `Component`
You can pass the {ComponentName: unknown or unknown[]}
type for the type argument. If you passed to the type argument, then the editor will suggest the correct type dependent on the passed type.
Note: If you are using ts-jest
, you need to combine it with babel
. You can check the way to set up config here. This is because, TypeScript
compiler can not compile named arrow functions correctly. You can read up on the issue here: microsoft/TypeScript#6433.
Tips
Performance
This library is using the Proxy
API to optimize testing speed. So you should use either renderCount
or renderTime
in a single test case. If you use both variables or you are testing a large component, the testing time will be a little slower.
Anonymous Component
If you are using an anonymous component, this library doesn't work correctly. To make this library work correctly, you need to set the display name as seen below.
React.memo(function MemoComponent() {
return <p>test</p>;
});
// or
const MemoComponent = () => <p>test</p>;
React.memo(MemoComponent);
Setting a display name will get benefits not only from this library but also when you debug in React.
Hooks
If you are using @testing-library/react-hooks
, you can check the number of renders with the perf
method as bellows.
const { renderCount } = perf(React);
const { result } = renderHook(() => {
/**
* use some hooks
*/
});
// You can get value from the TestHook component
wait(() => console.log(renderCount.current.TestHook.value));
This is because the renderHook
method is wrapping callbacks with the TestHook
component.