自己紹介
こんにちは。Standard Cognition( https://standard.ai )というSFにある会社でSoftware EngineerをやっているKentaです。
フロントエンド関連の事や、アメリカでのエンジニア経験などつぶやいていくので、ぜひTwitterのフォローもお願いします! @hellokenta_ja
1. 背景
Hooksなどの導入もあり、Reactのコンポーネントが非常に簡単に書けるようになりました。簡単に書ける分、知らず知らずのうちにアンチパターン的な実装をしてしまっている事がよくあると思っています。僕もReactを始めた当初たくさん地雷を踏んでいました、、、😭
そのような実装をしてしまった時に起こる問題としては
- 無駄なレンダリングが起きてパフォーマンスが落ちる
- 予期しないバグを生む
- アプリは通常に動いているように見えるので、上記は非常に気付きづらい
などがあります。
アプリケーションが小さいうちはパフォーマンスなどそこまで問題にならないのですが、コツコツと積み上がったパフォーマンスの負債は、アプリの規模が大きくなった時に現れてきます。
そんな僕も、初期にたくさん引っかかった実装のポイントをいくつか紹介していきます!
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
を持たなければいけなく、かつ子コンポーネントの再レンダーを防ぎたい場合は、memo
やuseMemo
を利用しましょう。
参考
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}</>
);
};
上記のように、useEffect
やuseState
が条件によって**呼ばれたり呼ばれなかったりする実装はやめましょう。**理由は、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 <></>
};
また、Array
やObject
のような参照型のものをDependencies
として持たせる場合も気をつけましょう。実際の中身は変わっていなくても、参照が変わっていたらuseEffect
の中の処理は呼ばれてしまいます。ですので、join
やJSON.stringfy
で文字列にするなどの工夫をする必要があります。
8. useSelectorで値をひとまとめにしない
const { name, age } = useSelector(state => state.user)
react-redux
を使う場合、useSelector
を利用してstate
から値を取ってくるのですが、useSelector
は===
によって同値性を判断しています。ですのでこのようにdestructive
に値を取得してしまうと、user
のReferenceが変更される度に、もしname
やage
の値が変更されていなくても、値が変わったとみなされて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表示のkey
にindex
を使用しない
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を利用して無駄なレンダーなどを特定しています。使い方などは割愛するので、下記を参考にお願いします。
- https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en
- https://ja.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html
- https://ja.reactjs.org/docs/optimizing-performance.html
12. まとめ
以上、Reactで初心者が気をつけてほしい実装ポイントでした。
僕は普段FPSを気にしたReactアプリケーションの実装をしているので大分ここら辺のパフォーマンスに注意して実装しているのですが、上記の事をケアすることによって不必要なAPIコールなどのバグも防げると思います。
最後まで読んでいただきありがとうございました。
Twitterでも、Reactやフロントエンドに関するツイートや、アメリカでのエンジニア経験に関してツイートしているので、よかったらフォローお願いします
Twitter: @hellokenta_ja