反応:あなたのアプリを殺している状態を持ち上げる


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 rowIsetContent .
    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
    うまくいけば、あなたのプロジェクトの役に立つ何かを見つけた.私にあなたのフィードバックを伝えるために無料!私は、最も確かにどんな批評と質問にでも感謝します.