タイトルの通りです。React の再レンダリングを介さずにDOMを直接更新する方が高FPSを出せるか実験してみたけど、そんなに変わらなかったという話です。
ことの発端
ストップウォッチを作っていて、こんな感じの ControlledComponent を書いていました。とてもオーソドックスなコードです。
type TimerState = 'idoling' | 'working';
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())}startbutton
) : (
button onClick={() => onFinish(performance.now())}stopbutton
)}
{currentTime && div{currentTime / 1000} secondsdiv}
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') {
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 に書き直すのはそんなに意味なさそう という検証でした。