👻

React Profilerの活用

2021/02/08に公開

この前、React レンダリング最適化(useMemo, useCallback, React.Memo)でも話したことはありますが、memo, useMemo,useCallbackの使用は性能の改善が必要と思う時に使うのが良いと述べました。そうしたら、改善が必要かどうかはどう判断したらいいでしょう?

React Developer Tools

ChromeでProfilerを使用するために以下のリンクからReact Developer Toolsをインストールします。
https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=ja

Profiler

Profilerを試すためにまず、5000枚のダミーイメージをレンダリングするコンポーネントを2つのバージョンに分けて作ってみます。
1つ目は下位コンポーネントに分けずに1個のコンポーネントで全てコンテンツをレンダリングする方法、
2つ目は下位コンポーネントに適切に分けてコンテンツをレンダリングする方法で作ってみます。

// src/components/PhotoListComponentV1.js

const PhotoListComponentV1 = ({ text, photoList }) => {
  return (
    <div>
      <h1>PhotoListComponentV1</h1>
      <p>text: {text}</p>
      <ul>
        {photoList.map((photo) => (
          <li key={photo.id}>
            <img src={photo.thumbnailUrl} alt={photo.title} />
          </li>
        ))}
      </ul>
    </div>
  );
};

export default PhotoListComponentV1;
// src/components/PhotoListComponentV2.js

const ShowText = ({ text }) => {
  return <p>text: {text}</p>;
};

const PhotoItem = ({ photo }) => {
  return (
    <li>
      <img src={photo.thumbnailUrl} alt={photo.title} />
    </li>
  );
};

const PhotoList = ({ photoList }) => {
  return (
    <ul>
      {photoList.map((photo) => (
        <PhotoItem key={photo.id} photo={photo} />
      ))}
    </ul>
  );
};

const PhotoListComponentV2 = ({ text, photoList }) => {
  return (
    <div>
      <h1>PhotoListComponentV2</h1>
      <ShowText text={text} />
      <PhotoList photoList={photoList} />
    </div>
  );
};

export default PhotoListComponentV2;
// src/App.js

import { useState, useEffect } from "react";
import PhotoListComponentV1 from "./components/PhotoListComponentV1";
import PhotoListComponentV2 from "./components/PhotoListComponentV2";

const App = () => {
  const [text, setText] = useState("");
  const [photoList, setPhotoList] = useState([]);

  // jsonplaceholderから5000枚のイメージデータをフェッチします。
  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/photos")
      .then((res) => res.json())
      .then(setPhotoList);
  }, []);

  return (
    <div style={{ margin: "32px", textAlign: "center" }}>
      <div>
        <label name="text">[text]</label>
        <input value={text} onChange={(e) => setText(e.target.value)} />
      </div>
      <div style={{ display: "flex", justifyContent: "space-around" }}>
        <PhotoListComponentV1 text={text} photoList={photoList} />
        <PhotoListComponentV2 text={text} photoList={photoList} />
      </div>
    </div>
  );
};

export default App;

サーバを起動し、ブラウザを開いてChromeのInspectモードからProfilerタブに移動します。
Start profilingボタンを押下してレコーディングをスタートします。

inputに内容(1、2,3)を入力した後Stop profilingを押下して結果を確認します。

Flamegraphチャートを見るとApp Componentのレンダリングには184.4ms、その中でPhotoListComponentV170.3msPhotoListComponentV2113.7msの時間かかったのが分かりました。

Rankedチャートを見るとコンポーネント毎のレンダリング所要時間順に並んでいる結果も確認できます。

ここまで見るとPhotoListComponentV1の方がPhotoListComponentV2のよりパフォーマンスが良いようですね。

React.memo適用後

ProfilerタブでView settingsボタンを押下して、Highlight updates when components render項目にチェックします。

これからはコンポーネントがレンダリングされる時、どの部分がレンダリングされるのかが視覚的に表示されます。
inputに内容(1、2、3、4、5)を入力してみると入力する度に、inputコンポーネントだけレンダリングすればいいのにすべてのコンポーネントが再レンダリングしてしまうのが分かります。

React.memoを使ってPhotoListComponentV1,PhotoListComponentV2を最適化してみます。

// src/components/PhotoListComponentV1.js

import { memo } from "react";

const PhotoListComponentV1 = ({ text, photoList }) => {
  return (
    <div>
      <h1>PhotoListComponentV1</h1>
      <p>text: {text}</p>
      <ul>
        {photoList.map((photo) => (
          <li key={photo.id}>
            <img src={photo.thumbnailUrl} alt={photo.title} />
          </li>
        ))}
      </ul>
    </div>
  );
};

// export default PhotoListComponentV1;
export default memo(PhotoListComponentV1);
// src/components/PhotoListComponentV2.js

import { memo } from "react";

// const ShowText = ({ text }) => {
//   return <p>text: {text}</p>;
// };

// const PhotoItem = ({ photo }) => {
//   return (
//     <li>
//       <img src={photo.thumbnailUrl} alt={photo.title} />
//     </li>
//   );
// };

// const PhotoList = ({ photoList }) => {
//   return (
//     <ul>
//       {photoList.map((photo) => (
//         <PhotoItem key={photo.id} photo={photo} />
//       ))}
//     </ul>
//   );
// };

// export default PhotoListComponentV2;

const ShowText = memo(({ text }) => {
  return <p>text: {text}</p>;
});

const PhotoItem = memo(({ photo }) => {
  return (
    <li>
      <img src={photo.thumbnailUrl} alt={photo.title} />
    </li>
  );
});

const PhotoList = memo(({ photoList }) => {
  return (
    <ul>
      {photoList.map((photo) => (
        <PhotoItem key={photo.id} photo={photo} />
      ))}
    </ul>
  );
});

const PhotoListComponentV2 = ({ text, photoList }) => {
  return (
    <div>
      <h1>PhotoListComponentV2</h1>
      <ShowText text={text} />
      <PhotoList photoList={photoList} />
    </div>
  );
};

export default memo(PhotoListComponentV2);

またProfilerを使った後、結果を確認すると、

レンダリング時間が69.7msPhotoListComponentV169.3msPhotoListComponentV20.2msで、PhotoListComponentV2が大幅に改善できたのが分かります。inputが変わる時、inputコンポーネントだけ再レンダリングするためです。

終わりに

レンダリング最適化のためにはコンポーネントを適切に分けて使用し、memo, useMemo, useCallbackを使えば良いと思います。ただ、本当に最適化が必要なのか判断するためには今回ご紹介したProfilerが役に立つかと思います。

参照

Discussion