Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

高FPSをたたき出すために UncontrolledComponent にしてみたけど、そんなに変わらなかった話

タイトルの通りです。React の再レンダリングを介さずにDOMを直接更新する方が高FPSを出せるか実験してみたけど、そんなに変わらなかったという話です。

ことの発端

ストップウォッチを作っていて、こんな感じの ControlledComponent を書いていました。とてもオーソドックスなコードです。

type TimerState = 'idoling' | 'working';

// ControlledStopwatch は prop で受け取った情報を描画するだけ。内部にステートを持たない
const ControlledStopwatch: FC<{
  onStart: (startAt: number) => void;
  onFinish: (endAt: number) => void;
  currentTime: number | null;
  state: TimerState;
}> = ({ onStart, onFinish, state, currentTime }) => {
  return (
    <div>
      {state === 'idoling' ? (
        <button onClick={() => onStart(performance.now())}>start</button>
      ) : (
        <button onClick={() => onFinish(performance.now())}>stop</button>
      )}
      {currentTime && <div>{currentTime / 1000} seconds</div>}
    </div>
  );
};

const Controlled: FC = () => {
  const [state, setState] = useState<TimerState>('idoling');
  const startAt = useRef(0);
  const [currentTime, setCurrentTime] = useState<number | null>(null);
  const fps = 120;
  useEffect(() => {
    if (state === 'working') {
      // 1000/fps ミリ秒ごとに表示を更新する
      const id = setInterval(() => {
        setCurrentTime(performance.now() - startAt.current);
      }, 1000 / fps);
      return () => clearInterval(id);
    }
  }, [fps, state]);

  return (
    <div className='App'>
      <ControlledStopwatch
        currentTime={currentTime}
        state={state}
        onStart={(val: number) => {
          setState('working');
          startAt.current = val;
        }}
        onFinish={(endAt: number) => {
          setState('idoling');
          console.log(endAt - startAt.current);
        }}
      />
    </div>
  );
};

このストップウォッチは、1000/fps ミリ秒ごとに setCurrentTime を行い、React の再レンダリングをしています。しかし、書き換えるべき箇所は時刻の部分のみです。ならば 再レンダリングはややコストが高いのでは? と思ったのが検証しようと思ったきっかけです。

UncontrolledComponent で書き直してみる。

UncontrolledComponent は state をコンポーネント内部で持っている(親がステートをコントロールできない)コンポーネントです。一見すると親でステートを持てないのはデメリットにしか見えませんが、逆にいえば 「親がステートを管理しなくていい」コンポーネント です。

親は子のレンダリングに関して一切の責任を負わないため、再レンダリングせずに直接 DOM を更新して書き換えても問題ありません。そのため、高FPSが求められる作業に向いていそうに思いました。結論をいうと大差なかったんですが。

というわけで書き直したデモがこちらになります。

codesandbox.io

結果: 目に見えた差はなかった

結果は上のデモをみてわかる通り、 目でみてわかるレベルの差はありませんでした 。60fpsで録画して確認してみたところ、どちらも60fps以上はバッチリ出ていたので、差を視認するのはかなり難しいはずです。

というわけで、 高いFPSを求めて UncontrolledComponent に書き直すのはそんなに意味なさそう という検証でした。