requestAnimationFrame と入力遅延の実測
ポインタイベントから描画までの経路を計測し、体感差がどこから生まれるかを切り分けます。
「押した感じが重い」という感覚は、たいてい数十ミリ秒の話です。 どこで時間が使われているのかを実際に測ってみました。
測る場所を決める
ポインタを押してから画面が変わるまでには、いくつか段階があります。
- 物理的な入力からブラウザがイベントを作るまで
- イベントハンドラが呼ばれてから、次の
requestAnimationFrameまで - 描画処理そのもの
- 描画完了から実際に画面に出るまで
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) |
イベント種別の変更が圧倒的に大きいという、身も蓋もない結論でした。 描画ループの最適化に手をつける前に、まず入力の取り方を見直すべきです。