反応:あなたのアプリを殺している状態を持ち上げる
53403 ワード
I now have a new shiny blog. Read this article with the latest updates there https://blog.goncharov.page/react-lifting-state-up-is-killing-your-app
あなたは“リフト状態”について聞いたことがありますか?私は、あなたがいると思います、そして、それはあなたがここにいる正確な理由です.どうしてそれが可能だろうかone of the 12 main concepts listed in React official documentation パフォーマンスが悪いかもしれない.この記事の中で、それが実際にケースであるとき、我々は状況を考慮します.
ステップ1:それを持ち上げる
私はチックタックトーの単純なゲームを作成することをお勧めします.我々が必要とするゲームのために:
undefined
, "x"
or "0".
const size = 10
// Two-dimensional array (size * size) filled with `undefined`. Represents an empty field.
const initialField = new Array(size).fill(new Array(size).fill(undefined))
const App = () => {
const [field, setField] = useState(initialField)
return (
<div>
{field.map((row, rowI) => (
<div>
{row.map((cell, cellI) => (
<Cell
content={cell}
setContent={
// Update a single cell of a two-dimensional array
// and return a new two dimensional array
(newContent) =>
setField([
// Copy rows before our target row
...field.slice(0, rowI),
[
// Copy cells before our target cell
...field[rowI].slice(0, cellI),
newContent,
// Copy cells after our target cell
...field[rowI].slice(cellI + 1),
],
// Copy rows after our target row
...field.slice(rowI + 1),
])
}
/>
))}
</div>
))}
</div>
)
}
const randomContent = () => (Math.random() > 0.5 ? 'x' : '0')
const Cell = ({ content, setContent }) => (
<div onClick={() => setContent(randomContent())}>{content}</div>
)
Live demo #1 これまではよく見えます.光の速度で相互作用できる完全に反応可能なフィールド:サイズを大きくしましょう.100まで.はい、それはそのデモのリンクをクリックして変更する時間です
size
非常に上の変数.まだあなたのための高速?200を試してみてください.今、あなたはセルのクリックとその内容が変わる時間の間の重要な遅れを見ますか?変えましょう
size
10に戻って、原因を調査するためにいくつかのプロファイリングを加えてください.const Cell = ({ content, setContent }) => {
console.log('cell rendered')
return <div onClick={() => setContent(randomContent())}>{content}</div>
}
Live demo #2 はい、それです.シンプル
console.log
すべてのレンダリングで実行すると十分です.それで、我々は何を見ますか?のセル数に基づいて
size
= Nでなければなりません.コンソールでは、一つのセルが変わるたびに、全てのフィールドが再描画されるようです.最も明白なことはいくつかのキーを追加することですReact documentation suggests .
<div>
{field.map((row, rowI) => (
<div key={rowI}>
{row.map((cell, cellI) => (
<Cell
key={`row${rowI}cell${cellI}`}
content={cell}
setContent={(newContent) =>
setField([
...field.slice(0, rowI),
[
...field[rowI].slice(0, cellI),
newContent,
...field[rowI].slice(cellI + 1),
],
...field.slice(rowI + 1),
])
}
/>
))}
</div>
))}
</div>
Live demo #3 しかし、増加した後
size
再び、我々はその問題がまだそこにあるのを見ます.コンポーネントがレンダリングする理由を見ることができれば.幸いにも、我々は驚くべきからいくつかの助けを借りてReact DevTools . なぜコンポーネントがレンダリングされるかを記録できる.手動でそれを有効にする必要があります.一旦それが可能になれば、私たちは、彼らの小道具が特に変化したので、すべての細胞が再描画されたのを見ることができます.
setContent
プロップ各セルには2つの小道具があります.
content
and setContent
. セル[ 0 ] [ 0 ]が変わるならば、セル[0][1]の内容は変わりません.一方、setContent
キャプチャfield
, cellI
and rowI
閉鎖中.cellI
and rowI
同じだが、field
すべてのセルの変更を変更します.コードをリファクタリングし続けましょう
setContent
同じ.参考にする
setContent
閉じるこの動画はお気に入りから削除されています.我々は排除できたcellI
and rowI
閉鎖によってCell
明示的パスcellI
and rowI
to setContent
. に関してfield
, 我々は、きちんとした特徴を利用することができましたsetState
- it accepts callbacks .const [field, setField] = useState(initialField)
// `useCallback` keeps reference to `setCell` the same.
const setCell = useCallback(
(rowI, cellI, newContent) =>
setField((oldField) => [
...oldField.slice(0, rowI),
[
...oldField[rowI].slice(0, cellI),
newContent,
...oldField[rowI].slice(cellI + 1),
],
...oldField.slice(rowI + 1),
]),
[],
)
どちらがApp
このように見える<div>
{field.map((row, rowI) => (
<div key={rowI}>
{row.map((cell, cellI) => (
<Cell
key={`row${rowI}cell${cellI}`}
content={cell}
rowI={rowI}
cellI={cellI}
setContent={setCell}
/>
))}
</div>
))}
</div>
現在Cell
パスしなければなりませんcellI
and rowI
にsetContent
.const Cell = ({ content, rowI, cellI, setContent }) => {
console.log('cell render')
return (
<div onClick={() => setContent(rowI, cellI, randomContent())}>
{content}
</div>
)
}
Live demo #4 devtoolsのレポートを見てみましょう.
え?なぜHeckはそれを言いますか?だから、私たちのフィールドが更新されるたびに
App
再描画されます.したがって、子コンポーネントは再描画されます.よろしい.StackOverflowは、反応性能最適化について何か役に立つと言いますか?インターネットの使い方shouldComponentUpdate
またはその近親者:PureComponent
and memo
.const Cell = memo(({ content, rowI, cellI, setContent }) => {
console.log('cell render')
return (
<div onClick={() => setContent(rowI, cellI, randomContent())}>
{content}
</div>
)
})
Live demo #5 ああ!そのコンテンツが変更されると、1セルのみが再描画されます.でも待ちます.何か驚いた?我々はベストプラクティスに続き、期待される結果を得た.
ここに悪笑いがあった.私はあなたと一緒にいないので、それを想像してください.先に行き、増加
size
インLive demo #5 . 今度は少しより大きい数で行かなければならないかもしれません.しかし、遅れはまだあります.なぜ?DevToolsレポートをもう一度見てみましょう.
たった一つしかない
Cell
そして、それはかなり高速だったが、レンダリングもありますApp
, それはかなりの時間がかかった.事は、すべての再レンダリングでApp
それぞれCell
その新しい小道具を前の小道具と比較しなければなりません.たとえそれがレンダリングされない(正確に私たちのケース)を決定しても、その比較はまだ時間がかかります.o ( 1 ),size
* size
タイムズ!ステップ2:それを下に移動
私たちはそれを回避するために何をすることができますか?レンダリング
App
あまりにも多くのコスト、我々はレンダリングを停止する必要がありますApp
. 我々の州を主催し続けるならば、それは可能でありませんApp
使用useState
, それは正確に何がトリガーを再レンダリングされます.それで、我々は我々の状態を下へ動かして、各々を行かなければなりませんCell
独自の状態を購読する.我々の国のための容器である専用のクラスをつくりましょう.
class Field {
constructor(fieldSize) {
this.size = fieldSize
// Copy-paste from `initialState`
this.data = new Array(this.size).fill(new Array(this.size).fill(undefined))
}
cellContent(rowI, cellI) {
return this.data[rowI][cellI]
}
// Copy-paste from old `setCell`
setCell(rowI, cellI, newContent) {
console.log('setCell')
this.data = [
...this.data.slice(0, rowI),
[
...this.data[rowI].slice(0, cellI),
newContent,
...this.data[rowI].slice(cellI + 1),
],
...this.data.slice(rowI + 1),
]
}
map(cb) {
return this.data.map(cb)
}
}
const field = new Field(size)
ではApp
以下のようになります.const App = () => {
return (
<div>
{// As you can see we still need to iterate over our state to get indexes.
field.map((row, rowI) => (
<div key={rowI}>
{row.map((cell, cellI) => (
<Cell key={`row${rowI}cell${cellI}`} rowI={rowI} cellI={cellI} />
))}
</div>
))}
</div>
)
}
アンドCell
からコンテンツを表示できますfield
独自にconst Cell = ({ rowI, cellI }) => {
console.log('cell render')
const content = field.cellContent(rowI, cellI)
return (
<div onClick={() => field.setCell(rowI, cellI, randomContent())}>
{content}
</div>
)
}
Live demo #6 この点で、我々は我々のフィールドが描かれているのを見ることができます.しかし、セルをクリックすると、何も起こりません.ログでは、クリックごとに「setCell」を見ることができますが、セルは空白のままです.ここでの理由は、何もセルが再表示するように指示しないことです.反応の外側の我々の状態は変化します、しかし、反応はそれについて知りません.それは変更しなければなりません.
どのようにプログラムをレンダリングするトリガできますか?
クラスでforceUpdate . クラスにコードを書き直す必要があるということですか?ではなく.機能的なコンポーネントでできることはいくつかのダミー状態を導入することです.
ここでどのように再描画を強制的にカスタムフックを作成することができます.
const useForceRender = () => {
const [, forceRender] = useReducer((oldVal) => oldVal + 1, 0)
return forceRender
}
再レンダリングするときに我々のフィールドの更新時に我々はそれが更新を知っている必要がトリガされます.それは我々が何らかのフィールド更新を購読することができることを意味します.class Field {
constructor(fieldSize) {
this.size = fieldSize
this.data = new Array(this.size).fill(new Array(this.size).fill(undefined))
this.subscribers = {}
}
_cellSubscriberId(rowI, cellI) {
return `row${rowI}cell${cellI}`
}
cellContent(rowI, cellI) {
return this.data[rowI][cellI]
}
setCell(rowI, cellI, newContent) {
console.log('setCell')
this.data = [
...this.data.slice(0, rowI),
[
...this.data[rowI].slice(0, cellI),
newContent,
...this.data[rowI].slice(cellI + 1),
],
...this.data.slice(rowI + 1),
]
const cellSubscriber = this.subscribers[this._cellSubscriberId(rowI, cellI)]
if (cellSubscriber) {
cellSubscriber()
}
}
map(cb) {
return this.data.map(cb)
}
// Note that we subscribe not to updates of the whole filed, but to updates of one cell only
subscribeCellUpdates(rowI, cellI, onSetCellCallback) {
this.subscribers[this._cellSubscriberId(rowI, cellI)] = onSetCellCallback
}
}
今、我々はフィールドの更新を購読することができます.const Cell = ({ rowI, cellI }) => {
console.log('cell render')
const forceRender = useForceRender()
useEffect(() => field.subscribeCellUpdates(rowI, cellI, forceRender), [
forceRender,
])
const content = field.cellContent(rowI, cellI)
return (
<div onClick={() => field.setCell(rowI, cellI, randomContent())}>
{content}
</div>
)
}
Live demo #7 遊びましょう
size
この実装で.前にlaggy感じた値にそれを増加してみてください.そしてそれはシャンパンの良いボトルを開く時間です!私たちは自分自身のアプリを1つのセルと1つのセルを唯一のときにそのセルの状態を変更するレンダリング!devtoolsのレポートを見てみましょう.
今見る限り
Cell
レンダリングされているとそれはクレイジークレイジーです.今の私たちのコードを言うなら
Cell
メモリリークの潜在的原因は?ご覧の通り、中でuseEffect
我々はセルの更新を購読するが、我々は決して取り消すことはありません.それはCell
が破棄され、サブスクリプションがオンになっています.それを変えましょう.まず、教える必要がある
Field
それが何を約束することを約束します.class Field {
// ...
unsubscribeCellUpdates(rowI, cellI) {
delete this.subscribers[this._cellSubscriberId(rowI, cellI)]
}
}
これで申し込むunsubscribeCellUpdates
我々にCell
.const Cell = ({ rowI, cellI }) => {
console.log('cell render')
const forceRender = useForceRender()
useEffect(() => {
field.subscribeCellUpdates(rowI, cellI, forceRender)
return () => field.unsubscribeCellUpdates(rowI, cellI)
}, [forceRender])
const content = field.cellContent(rowI, cellI)
return (
<div onClick={() => field.setCell(rowI, cellI, randomContent())}>
{content}
</div>
)
}
Live demo #8 では、ここでのレッスンは何ですか?コンポーネントツリーの状態を移動するには、いつそれを意味しますか?決して!彼らは失敗し、任意の未熟な最適化を行うまでベストプラクティスにスティック.正直に言って、上記のケースはいくぶん特定です、しかし、あなたが本当に大きなリストを表示する必要があるならば、あなたはそれを思い出すでしょう.
ボーナスステップ:リアルワールドリファクタリング
にlive demo #8 我々はグローバルを使用
field
, 現実世界のアプリの場合ではありません.それを解決するために、我々はホストできましたfield
我々の中でApp
を使用してツリーを下に渡すcontext .const AppContext = createContext()
const App = () => {
// Note how we used a factory to initialize our state here.
// Field creation could be quite expensive for big fields.
// So we don't want to create it each time we render and block the event loop.
const [field] = useState(() => new Field(size))
return (
<AppContext.Provider value={field}>
<div>
{field.map((row, rowI) => (
<div key={rowI}>
{row.map((cell, cellI) => (
<Cell key={`row${rowI}cell${cellI}`} rowI={rowI} cellI={cellI} />
))}
</div>
))}
</div>
</AppContext.Provider>
)
}
今私たちは消費することができますfield
文脈からCell
.const Cell = ({ rowI, cellI }) => {
console.log('cell render')
const forceRender = useForceRender()
const field = useContext(AppContext)
useEffect(() => {
field.subscribeCellUpdates(rowI, cellI, forceRender)
return () => field.unsubscribeCellUpdates(rowI, cellI)
}, [forceRender])
const content = field.cellContent(rowI, cellI)
return (
<div onClick={() => field.setCell(rowI, cellI, randomContent())}>
{content}
</div>
)
}
Live demo #9 うまくいけば、あなたのプロジェクトの役に立つ何かを見つけた.私にあなたのフィードバックを伝えるために無料!私は、最も確かにどんな批評と質問にでも感謝します.
Reference
この問題について(反応:あなたのアプリを殺している状態を持ち上げる), 我々は、より多くの情報をここで見つけました https://dev.to/itnext/react-lifting-state-up-is-killing-your-app-33maテキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol