EY-Office ブログ

React 18への予習シリーズ: Automatic batching

今回のReact 18への予習シリーズはAutomatic batchingです。

実はReactのbatchingという言葉はつい最近まで知りませんでした、しかしその動きは知っていました。さらにReact17.Xまでのbatchingに問題点があることも知りませんでした。😅

Automatic batching Wikimedia Commons: Multiple Silos Single Weigh Static Automatic Batching System.jpg

batchingとは

以下の簡単なReactのコードはボタンを押すと、numberとrepeatのステータスが更新されます(そして再表示されます)。
このコードでLogコンポーネントはコンソールにRenderと表示するだけのコンポーネントです。これがあることで再表示が何度起きたかがわかります。

Reactではステートが変更されると再表示されます。しかしステートの変更、以下のコードではsetNumber、setRepeat関数が呼び出されるたびに再表示が行われると、表示速度が遅くなり良いUXを提供できなくなります。

このコードでボタンを押すと、2つのステートが更新されます、しかしRenderは1回だけ表示されます。
このように複数のステート変更が行われても1回しか再表示が行われない仕掛けをbatchingと呼ばれています。この動作はReactを使った事のある方は知って(体験して)いると思います。

import React, { useState } from 'react'
import ReactDOM from 'react-dom'

const Log = () => {
  console.log("Render");
  return null;
}

const App: React.FC = () => {
  const [number, setNumber] = useState(0)
  const [repeat, setRepeat] = useState(1)

  const incement = () => {
    setNumber(number + 1)
    setRepeat(repeat + 1)
  }

  return (
    <>
      <p>{String(number).repeat(repeat)}</p>
      <button onClick={incement}>+</button>
      <Log/>
    </>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

batchingの限界

しかし、React17.X以下ではbatchingの動作しないケースがあります、下のコードはその例です。

ステートの2つの変更がsetTimeout関数のコールバック内にあると、Renderは2回表示されます。ステート更新毎に再表示が行われているのです。

batchingの動作しないケースは、

  • Promise
  • setTimeout
  • addEventListener()で設定したイベントハンドラー

ということで、fetch等を使った通信処理で結果を複数のステート変更に使いコードは良くあると思いますが、ここではステートの更新毎に再表示が行われています!

import React, { useState } from 'react'
import ReactDOM from 'react-dom'

const Log = () => {
  console.log("Render");
  return null;
}

const App: React.FC = () => {
  const [number, setNumber] = useState(0)
  const [repeat, setRepeat] = useState(1)

  const incement = () => {
    setTimeout(() => {
      setNumber(number + 1)
      setRepeat(repeat + 1)
    }, 1000)
  }

  return (
    <>
      <p>{String(number).repeat(repeat)}</p>
      <button onClick={incement}>+</button>
      <Log/>
    </>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

なぜこうなるのか?

React 17.X以下のbatchingが動作するのは、

  • onClick, onChange...等のイベントはReactのライブラリー内で処理されますのでステート変更処理がBatching方式になります
  • Batching方式ではステートの変更はReact内のキューに変更情報が追加されるだけです
  • イベント処理の最後でキューに変更情報があれば再表示が1回だけ行われます

しかし、Promiseの処理コードやaddEventListener()で設定したイベントハンドラーではステート変更処理が、batching方式になっていないのでステート変更毎に再表示が実行されてしまします。

React18 Automatic batching

React 18で以下のように並列モード(Concurrent Mode)を有効にすると、Promiseの処理コードやsetTimeout、addEventListenerで設定したイベントハンドラーでもbatchingが動作しまます(Render表示は1回だけになります)。

-  ReactDOM.render(<App />, document.getElementById('root'))
+  ReactDOM.createRoot(
+    document.getElementById('root') as HTMLElement).render(<App />)

並列モード(Concurrent Mode)の表示処理Fiberは、①メモリー上の仮想DOM構築と、②それの結果を使いブラウザー画面(DOM)を更新する処理に、分かれています。

仮想DOM構築処理は小さい単位で中断(終了・再開)できるようになっています、そしてブラウザー画面を更新処理は画面のリフレッシュレート(16mSec)毎のみ行われます。
並列モード(Concurrent Mode)ではステート変更処理毎に仮想DOMは変更されますが、再表示は16mSecに1回しか行われません。したがってほとんどの場合はbatching動作します。

Fiberに付いては、A deep dive into React Fiber internalsに詳しく書かれています。
このブログでも薦めている Lin Clark - A Cartoon Intro to Fiber - React Conf 2017 はFirberの内部を漫画を使い分かりやすく解説しています。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ