11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

React初心者が気をつけたいコンポーネント実装のポイント、パフォーマンスチューニング

Last updated at Posted at 2020-05-06

自己紹介

こんにちは。Standard Cognition( https://standard.ai )というSFにある会社でSoftware EngineerをやっているKentaです。

フロントエンド関連の事や、アメリカでのエンジニア経験などつぶやいていくので、ぜひTwitterのフォローもお願いします! @hellokenta_ja

1. 背景

Hooksなどの導入もあり、Reactのコンポーネントが非常に簡単に書けるようになりました。簡単に書ける分、知らず知らずのうちにアンチパターン的な実装をしてしまっている事がよくあると思っています。僕もReactを始めた当初たくさん地雷を踏んでいました、、、😭

そのような実装をしてしまった時に起こる問題としては

  • 無駄なレンダリングが起きてパフォーマンスが落ちる
  • 予期しないバグを生む
  • アプリは通常に動いているように見えるので、上記は非常に気付きづらい

などがあります。
アプリケーションが小さいうちはパフォーマンスなどそこまで問題にならないのですが、コツコツと積み上がったパフォーマンスの負債は、アプリの規模が大きくなった時に現れてきます。
そんな僕も、初期にたくさん引っかかった実装のポイントをいくつか紹介していきます!:tada:

2. まず、Reactコンポーネントはどんな時に再レンダーされるのか理解しよう

パフォーマンスを改善するには、まずReactコンポーネントがどのタイミングで再レンダーされるのかを理解する必要があります。Reactコンポーネントが再レンダーされるのは下記のタイミングです。

  • 新しいpropsを受け取った時
  • stateが変更された時
  • リッスンしているContextの値が変更された時
  • 親のコンポーネントが上記の理由で再レンダーされた時

参考
https://stackoverflow.com/questions/55106951/react-with-hooks-when-do-re-renders-happen

かつ、かつ再レンダーとアンマウントは別です。
再レンダーとは、コンポーネントに対してrender関数を呼び出す事であり、stateは保持されます。アンマウントされると、そのコンポーネントのインスタンスは破棄され、stateも破棄されます。
参考
https://ja.reactjs.org/docs/reconciliation.html

3. Stateはなるべく子コンポーネントで持とう

上記で書いたように、親コンポーネントが再レンダーされると、子コンポーネントまで再レンダーされてしまいます。なので、なるべく親コンポーネントでstateを管理するのはやめて、最低限そのデータが必要な子コンポーネントでそのデータを読むようにしましょう。
例えば

  • /products/:product_idのページ全体に対応する親コンポーネントでproduct_idを取得する
  • そのProductのデータを取ってくるAPIコールは親コンポーネントで行う
  • 子コンポーネントにはproduct_idだけPropsとして渡し、そのproduct_idに対応するデータをRedux Stateなどから取得する

という感じです。子コンポーネントにpropsとしてデータを渡さない事は、コンポーネントとして汎用性が低くなり、テストがしづらくなるなどトレードオフもありますので、そこはバランスを見て実装しましょう。

どうしても親でstateを持たなければいけなく、かつ子コンポーネントの再レンダーを防ぎたい場合は、memouseMemoを利用しましょう。
参考
https://medium.com/@guptagaruda/react-hooks-understanding-component-re-renders-9708ddee9928

4. Renderメソッド内でコンポーネントやコンポーネントのスコープとは関係のない関数を作ったりしない

Before

const Sample = () => {
  const Inner = ({ value }) => {
    return <div>Value is {value}</div>
  }
  const add = (a, b) => a + b;

  return (
    <>
      <Inner value={add(1,2)} />
    </>
  )
}

上記の実装だと、Sampleがrenderされる度にInnerのインスタンスは新しく作られてマウントされてしまします。同様にaddメソッドも無駄に作り直されてしまいます。

After

const Inner = ({ value }) => {
  return <div>Value is {value}</div>
}

const add = (a, b) => a + b;

const Sample = () => {
  const value = add(1,2)
  return (
    <>
      <Inner value={value} />
    </>
  )
}

5. useEffectやuseStateは条件分岐の中で呼び出してはいけない

const Sample = (props) => {
  if (!props) {
    useEffect(() => {
      //なにかの処理
    });
  }
  if (!props) return <div>早期リターン</div>

  const [name, setName] = useState('Kenta')
  return (
    <>{name}</>
  );
};

上記のように、useEffectuseStateが条件によって**呼ばれたり呼ばれなかったりする実装はやめましょう。**理由は、React内部の実装が、それらが呼ばれる順番に依存した実装をしているからです。
参考
https://ja.reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

6. APIコールなどは、useEffect内で呼び出し、ちゃんとDependenciesを設定する

APIコールなどの関数呼び出しをコンポーネントの関数内で直接呼ぶのは、render毎に呼ばれてしまうので避けましょう。
また、useEffect内で呼び出したとしても、Dependenciesを設定しないと、render毎にその処理が呼ばれてしまいます。

なので**「何の値が変更されたらその処理を走らせたいのか」**を考慮して、適切なDependenciesを設定しましょう。APIコールなど、マウント時に一度しか呼ばなくて良い処理は[]Dependenciesに設定しましょう。

const Sample = (props) => {
  const dispatch = useDispatch()
  useEffect(() => {
    dispatch(getProducts()) // APIコール処理
  }, []) // <- これがDependencies。マウント時に一度だけ呼ばれる

  return <></>
};

7. useEffectのDependenciesにFunctionやArrayを渡す際の注意点

const Sample = (props) => {
  const fetchData = async () => {
    // fetch data code
  }

  useEffect(() => {
    fetchData()
  }, [fetchData])

  return <></>
};

上記の例はうまく動きません。
なぜなら、render毎にfetchDataは新しく作られるので、useEffect内の処理もrender毎に呼ばれます。結果的に、render毎にAPIコールを呼んでしまうことになります。

解決策としては、

fetchData関数をuseEffect内に入れる。

const Sample = (props) => {
  useEffect(() => {
    const fetchData = () => {
      // fetch data code
    }
    fetchData()
  }, [])

  return <></>
};

fetchDataがコンポーネントのスコープと関係ないなら外に出してしまう。

const fetchData = () => {
  // fetch data code
}
const Sample = (props) => {
  useEffect(() => {
    fetchData()
  }, [])

  return <></>
};

また、ArrayObjectのような参照型のものをDependenciesとして持たせる場合も気をつけましょう。実際の中身は変わっていなくても、参照が変わっていたらuseEffectの中の処理は呼ばれてしまいます。ですので、joinJSON.stringfyで文字列にするなどの工夫をする必要があります。

8. useSelectorで値をひとまとめにしない

const { name, age } = useSelector(state => state.user)

react-reduxを使う場合、useSelectorを利用してstateから値を取ってくるのですが、useSelector===によって同値性を判断しています。ですのでこのようにdestructiveに値を取得してしまうと、userのReferenceが変更される度に、もしnameageの値が変更されていなくても、値が変わったとみなされてrenderが走ってしまいます。

なので、下記のようにそれぞれでuseSelectorを呼び出す必要があります。

const name = useSelector(state => state.user.name)
const age = useSelector(state => state.user.age)

参考
https://react-redux.js.org/api/hooks#useselector

9. List表示のkeyindexを使用しない

indexを利用してしまっている例

{products.map((product, index) =>
  <Product
    {...product}
    key={index}
  />
)}

Reactは、List表示のレンダーを検討する際、子要素を順番に見ていって、差分を見つけたところで更新を発生させます。順番に依存しているので、ほとんどの要素は変更するべきではないのに、順番が変わっただけで全ての子要素を再レンダーしてしまう事もありえます。

ですので、keyにはその要素に対して一意なもの(uuidなど)を渡しましょう。

10. ReduxのStateをノーマライズしましょう

こちらはあくまで実装のコストなどのバランスも検討して必要に応じて、という事になりますが、state自体の最適化をする事によって、不必要なレンダーを防ぐことも出来ます。

Before

const products = [
  {
    id: 'product1',
    producer: { name: 'Kenta inc', id: 'kenta' },
    skus: [
      {
        id: 'sku1',
        name: 'White T shirt'
      },
      {
        id: 'sku2',
        name: 'Black T shirt'
      }
    ]
  },
  {
    id: 'product2',
    producer: { name: 'Hara inc', id: 'hara' },
    skus: [
      {
        id: 'sku3',
        name: 'White cap'
      },
      {
        id: 'sku4',
        name: 'Black cap'
      }
    ]
  },
]

After

const reducer = {
  products: {
    byId: {
      "product1": {
        id: "product1",
        producer: "kenta",
        skus: ["sku1", "sku2"]
      },
      "product2": {
        id: "product2",
        producer: "hara",
        skus: ["sku3", "sku4"]
      }
    },
    allIds: ["product1", "product2"]
  },
  producers: {
    byId: {
      "kenta": {
        id: "kenta",
        name: "Kenta inc"
      },
      "hara": {
        id: "hara",
        name: "Hara inc",
      }
    },
    allIds: ["kenta", "hara"]
  },
  skus: {
    buId: {
      "sku1": {
        id: "sku1",
        name: "White T shirt"
      },
      "sku2": {
        id: "sku2",
        name: "Black T shirt"
      },
      "sku3": {
        id: "sku3",
        name: "White cap"
      }
    },
    allIds: ["sku1", "sku2", "sku3"]
  }
}

このようにノーマライズすることによって、

  • データのネストが少なくなったのでデータの取得や更新が簡単になる
  • データが分離されたので、データに更新があった時にUIの変更が少なくすむ
    • 例えばallIdsの配列の順番が変更されただけでは、データの詳細にあたるbyIdの変更は無いので、それに対応するUIの変更はなくなります。

参考
https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape

11. パフォーマンス・チューニングの仕方

僕は主にReactのDeveloper Toolを利用して無駄なレンダーなどを特定しています。使い方などは割愛するので、下記を参考にお願いします。

12. まとめ

以上、Reactで初心者が気をつけてほしい実装ポイントでした。
僕は普段FPSを気にしたReactアプリケーションの実装をしているので大分ここら辺のパフォーマンスに注意して実装しているのですが、上記の事をケアすることによって不必要なAPIコールなどのバグも防げると思います。

最後まで読んでいただきありがとうございました。
Twitterでも、Reactやフロントエンドに関するツイートや、アメリカでのエンジニア経験に関してツイートしているので、よかったらフォローお願いします:tada:
Twitter: @hellokenta_ja

11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?