先日、react-performance-testing という React でレンダリング回数やレンダリングにかかる時間をテストできるライブラリを開発しました🎉
Reactをテストする時にレンダリング回数をテストしたいと思ったことがある人は多いはずです。実際、パフォーマンスを意識して開発しているときや、動作が重くなりやすいコンポーネントを開発している時には、レンダリング回数をテストして、自動で確認できるようにした方が開発効率はよくなります。今回開発した react-performance-testing はそのような課題を解決し、React におけるランタイムのパフォーマンスをユニットテストで簡単にテストできるライブラリーです。
使い方
インストール
npm:
npm install --save-dev react-performance-testing
yarn:
yarn add --dev react-performance-testing
レンダリング数をテストする
レンダリング数をテストするための基本的な使い方は、以下のような実装になります。
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import { perf, wait } from 'react-performance-testing';
test('countボタンをクリックした時に、2回レンダリングする', async () => {
const Button = ({ name, onClick }) => (
<button type="button" onClick={onClick}>{name}</button>
);
const Counter = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<span>{count}</span>
<Button name="count" onClick={() => setCount(c => c + 1)}/>
</div>
);
};
// perf メソッドの内部で createElement にモンキーパッチを当てているので、
// React モジュールを引数に渡す必要があります。
const { renderCount } = perf(React);
fireEvent.click(screen.getByRole('button', { name: /count/i }));
// renderCount.current 以下に各コンポーネントの名前がkeyとなるオブジェクトを含んでいて、
// さらにその中の value プロパティにレンダリング数が含まれています。
await wait(() => {
expect(renderCount.current.Counter.value).toBe(2);
expect(renderCount.current.Button.value).toBe(2);
});
});
上記のような形でレンダリング数をテストすることができます。
レンダリングにかかる時間をテストする
レンダリングにかかる時間をテストするための基本的な使い方は、以下のような実装になります。
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import { perf } from 'react-performance-testing';
test('countボタンをクリックした時に、レンダリングにかかる時間が16ミリ秒以下になる', async () => {
const Button = ({ name, onClick }) => (
<button type="button" onClick={onClick}>{name}</button>
);
const Counter = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<span>{count}</span>
<Button name="count" onClick={() => setCount(c => c + 1)}/>
</div>
);
};
const { renderTime } = perf(React);
fireEvent.click(screen.getByRole('button', { name: /count/i }));
await wait(() => {
// renderCount.current 以下に各コンポーネントの名前がkeyとなるオブジェクトを含んでいて、
// さらにその中の mount プロパティと updates プロパティにレンダリングにかかる時間が含まれています。
// 値はミリ秒単位で渡されます。
expect(renderTime.current.Counter.mount).toBeLessThan(16); // 初期レンダリング
// updatesは配列
expect(renderTime.current.Counter.updates[0]).toLessThan(16); // state更新
});
上記のような形でレンダリングにかかる時間をテストできます。渡される値はミリ秒単位であることに注意してください。
mount
プロパティには初期レンダリング(マウント時)の時間を計測した結果が含まれます。
updates
プロパティは配列で渡されます。このプロパティにはstateを更新した時などに発生する、再レンダリングにかかる時間の計測結果が格納されています。
ここでの課題としてはupdates
プロパティをテストする時の体験がいまいちなところです。これに関しては、今後Utilメソッドのようなものを開発するか、@testing-library/jest-dom のような、jest
を拡張するライブラリを開発する予定です。
注意: renderTime
を使う場合、テストは一つずつ行う必要があることに注意してください。計測プロセスには、Inline CachingというV8の最適化処理が入ってしまうため、最初のテストケースの結果と2回目のテストケースの結果が大きく変わってしまう可能性があるからです。Inline Cashingについてはこの記事がわかりやすいです。
複数のコンポーネントを含む場合
複数のコンポーネントを含む場合は、renderCount
とrenderTime
に含まれるコンポーネントは表示される順に配列で渡されます。
const Text = ({ text }) => <p>{text}</p>;
const Component = () => (
<div>
<Text text="1"/>
<Text text="2"/>
<Text text="3"/>
</div>
);
const { renderCount } = perf(React);
wait(() => console.log(renderCount.current));
/*
* {
* Component: { value: 1 },
* Text: [{ value: 1 }, { value: 1 }, { value: 1 }]
* }
*/
複数のコンポーネントを含む場合、上記のように配列で渡されます。そして、各コンポーネントのプロパティはネストしているコンポーネントを区別せずにフラットな状態で渡されます。これは、このライブラリが小さな単位(ListやModalなど)でコンポーネントをテストするために作られたものだからです。小さなコンポーネントをテストするライブラリであるため、ネストした状態で値を保持するのは、コード量が増えたり、テストしにくくなってしまうと考えたためこのような設計になっています。
例えば、
expect(renderCount.current.Component.value).toBe(1);
expect(renderCount.current.Component.Text[0].value).toBe(1);
とするよりも
expect(renderCount.current.Component.value)toBe(1);
expect(renderCount.current.Text[0].value)toBe(1);
のように書けた方が、すぐにコンポーネントにアクセスできてテストを書きやすいと考えました。
そのため、Pageコンポーネントなどの多くの重複するコンポーネントを含む大きなコンポーネントをテストするケースでは使い勝手が悪いです。
しかし、今後、大きなコンポーネントでも開発体験を向上できるように改善していく予定です。
良い方法があればissueまたはPRを送っていただければ嬉しいです
推奨テストライブラリ
react-performance-testing を使う時に他のテストライブラリと併用して使うと思います。
Jest, Mocha, Jasmine では自動でクリーンアップなどの処理を行うため、これらのライブラリを合わせて使うことをお勧めします。これ以外のライブラリを使っている場合は、teardownのタイミングで react-performance-testing のcleanup
メソッドを実行してください。
TypeScript
TypeScript にも対応していて、型安全なテストを実装することができます。
const Text = ({ text }) => <p>{text}</p>;
const Component = () => (
<div>
<Text text="1"/>
<Text text="2"/>
<Text text="3"/>
</div>
);
const Component = () => <div>test</div>;
const { renderCount } = perf<{ Component: unknown, Text: unknown[] }>(React);
Generics を使ってコンポーネントの名前をkeyに持つunknown
型を定義することで、そのコンポーネントが配列になるのか、単純なオブジェクトなのかを定義できます。
この方法についてはもっと良いやり方がありそうなので、issueまたはPRお待ちしています
ReactNative
ReactNativeでも使うことができます。
ReactNativeで使うには、
import { perf } from 'react-performance-testing/native';
のようにインポートすることで、ReactNativeに対応したperf
メソッドを使うことができます。
ざっくりとした仕組み
perf
メソッドにはReact
モジュールを渡しています。これは、React.createElement
にモンキーパッチを当てて、渡されたコンポーネントを独自の関数でラップして、レンダリング回数やレンダリングにかかる時間を計測するようにしているからです。このような仕組みであるため、React
モジュールを引数に渡す必要があります。
最後に
ここまで読んでいただきありがとうございました!
是非使ってみてください。
そしてフィードバックやGitHubにをつけてくれたら、モチベーションになります
GitHub: https://github.com/keiya01/react-performance-testing
追記(2020/08/21)
v1.2.0 で Breaking Changes が入りました。今まではrenderCount
などの変数を直接使うことができていましたが、今後は wait
メソッドでラップする必要があります。
変更の経緯は、これまでは値の計測および変数への代入などの処理をレンダリング処理中に行っていました。この処理はわずかな処理であるため、通常はそこまで大きな誤差にはならないのですが、大きいコンポーネントや複雑な処理を含む場合、renderTime
に大きく影響する可能性があるため、レンダリング処理が終わったタイミングで値の計測などの処理を行うように変更しました。