← コラム一覧へ
PERF2026.06.02読了 3 分 · デモあり

requestAnimationFrame と入力遅延の実測

ポインタイベントから描画までの経路を計測し、体感差がどこから生まれるかを切り分けます。

「押した感じが重い」という感覚は、たいてい数十ミリ秒の話です。 どこで時間が使われているのかを実際に測ってみました。

測る場所を決める

ポインタを押してから画面が変わるまでには、いくつか段階があります。

  1. 物理的な入力からブラウザがイベントを作るまで
  2. イベントハンドラが呼ばれてから、次の requestAnimationFrame まで
  3. 描画処理そのもの
  4. 描画完了から実際に画面に出るまで

JS から測れるのは 2 と 3 です。1 と 4 はブラウザとディスプレイの領分で、 こちらからは触れません。ただし 2 は設計次第で大きく変わります。

イベントから次フレームまで

イベントの timeStamp と、次の rAF コールバックの引数は同じ時間軸なので、そのまま引けます。

canvas.addEventListener('pointerdown', (e) => {
  const evtTime = e.timeStamp;
  requestAnimationFrame((frameTime) => {
    console.log('event → frame:', (frameTime - evtTime).toFixed(1), 'ms');
  });
});

手元で測ると、この値は 1ms から 16ms の間でばらつきました。 これは当然で、イベントがフレームのどのタイミングで来たかによります。 60Hz なら平均で 8ms 程度、最悪で 16ms 待つことになります。

つまり rAF で描いている限り、平均 8ms の遅延は原理的に避けられません。 ここを削るには 120Hz のディスプレイに載せるしかなく、コード側でできることはありません。

削れるのは別のところ

代わりに効いたのは、次の 2 点でした。

状態更新を待たずに描く。 React の setState で状態を持つと、 再レンダーを経由してから描画されるため、1 フレーム余分に遅れることがあります。 当たり判定の視覚フィードバック(クリック地点のリング)は state に入れず、 描画ループが直接読む配列に積むようにしました。

// state に入れず、描画ループが読む配列へ直接積む
hits.push({ x, y, t: now });

スコアのような「1 フレーム遅れても分からない」ものは state のままで問題ありません。 遅れが見えるのは、押した瞬間に出したい演出だけです。

pointerdown を使う。 click はボタンを離すまで発火しません。 押した瞬間に反応させたいなら pointerdown です。これだけで、 押してから離すまでの時間(人によっては 50ms 以上)が丸ごと消えます。

結局どうだったか

対策 削れた時間
click → pointerdown 40〜80ms
演出を state から外す 8〜16ms
rAF の待ち 削れない(平均 8ms)

イベント種別の変更が圧倒的に大きいという、身も蓋もない結論でした。 描画ループの最適化に手をつける前に、まず入力の取り方を見直すべきです。