はじめに
R3FのレンダリングループとWebXRはなぜ噛み合わないのか?
この記事は R3Fの内部構造 を“XRの視点”から解説することで、 普通は絶対に気づけない落とし穴を全部暴く。
前回の記事:
[Next.js #03] React Three Fiber(R3F)導入:Three.js を Next.js と正しく統合する
Next.js #03 は、Three.js を Next.js(App Router) で動かす際に必須となる React Three Fiber(R3F)の導入編。R3F が何をして何をしないのか、Three.js との対応 …
https://humanxai.info/posts/nextjs-03-r3f-introduction/1. なぜ R3F × WebXR は“理論上”ぶつかるのか?
React Three Fiber(R3F)は、React の state と Three.js の世界を同期させるフレームワークだ。 そのために、R3F は独自の「レンダリングループ(render loop)」を持ち、Canvas 内の描画進行を完全に管理しようとする。
- React の state 更新
- R3F の内部 clock
- useFrame の実行順
- gl.render() の発火タイミング
これらすべてを R3F が主導するという思想で設計されている。
しかし、WebXR はまったく逆の構造を持つ。
WebXR ではブラウザが、 XRSession 専用の animation loop(requestAnimationFrame) を所有している。
- XR デバイス(Quest など)が持つ固有の FPS(72Hz / 90Hz / 120Hz)
- HMD の姿勢トラッキング
- コントローラーの入力
- IPD / projection 計算
- ルームスケール座標の更新
これらすべてを ブラウザ側が絶対的な主導権を持つ。
結論:どちらも「主役になろうとする」
→ R3F:WebGL のフレーム進行を握りたい
→ WebXR:XRSession のフレーム進行を握りたい
この瞬間に 2つの “render loop の主導権争い” が発生する。
Three.js の内部動作がさらに問題を悪化させる
Three.js は XR に入った瞬間、
gl.setAnimationLoop( XR の animationLoop )
を内部で自動的に呼ぶため、 レンダリングループが XR に乗っ取られる。
つまり:
◎ XR → gl.render() の支配権を獲得
◎ R3F → 主導権を失い、useFrame だけ別ループで動く
###「useFrame がズレた別ループで回る」のが最大の致命傷
R3F の useFrame() は本来、 R3F 自身が管理する render loop の一部だ。
しかし XR では:
- R3F の loop は 非XR的な 60Hz 近辺
- XR の loop は デバイス依存の 72〜120Hz
つまり…
→ useFrame の delta が XR と一致しない
→ カメラや VRM の更新が“1フレーム遅れる”
→ 酔いやすくなる
→ パフォーマンスが不安定になる
→ コントローラーの動きに遅延が出る
特に VRM・MMD など “アニメーションがシビアなモデル” は顕著だ。
まとめ
R3F と WebXR は設計レベルでこうなっている:
- どちらも自分の render loop を主導したい
- Three.js が XR 側の animationLoop に乗っ取られる
- useFrame が XR のフレームレートと同期しない
- 結果:XR 内でズレ・遅延・ブラックアウトが発生する
これは“バグ”ではなく、構造的に必然の衝突だ。
2. R3F Canvas の独自 render loop が XR と干渉する
R3F(React Three Fiber)は、Canvas 内部で React の state 管理と Three.js の描画を統合するための独自ループを持っている。
その内部構造を極端に簡略化すると、以下のようになる。
Canvas
└─ renderLoop()
├─ advance(state) // React / R3F の世界を進める
├─ state.clock.update() // 時間管理(R3F専用)
├─ run useFrame callbacks // R3F のフック
└─ gl.render(scene, camera) // 最終描画
このループは React の再レンダ → R3F の state 更新 → useFrame の実行 という流れをひとまとめにしており、 R3F の“主導権”で進むように設計されている。
一方、WebXR は完全に別の世界で動いている
WebXR は一般的な requestAnimationFrame() ではなく、
XR デバイス固有のレートで動く専用の animation loop を持つ。
navigator.xr.requestAnimationFrame( timestamp )
└─ renderer.setAnimationLoop( XRLoop )
├─ updateXRPose() // HMD の姿勢
├─ updateControllers() // コントローラー入力
└─ renderer.render() // 最終描画(XR視点)
これは Three.js が XR モードに入ると 自動的に setAnimationLoop() を XR に渡すため、 Three.js のレンダリング権限は XR に完全に移る。
“R3F の renderLoop() と XR の renderLoop()” は 別宇宙
R3F と WebXR のループは:
タイミングも
フレームレートも
timestamp も
更新順も
####責任を持つ処理も
すべて違う。
つまり、
両者は同期しない。できる構造になっていない。
根本的な問題:R3F の clock は XR の timestamp を知らない
R3F は内部で state.clock を持っている。
これは:
- React の状態更新
- useFrame の delta
- R3F のアニメーション制御
- invalidate / advance のタイミング
などに使われる重要なオブジェクトだが、
→ XR の animationLoop の timestamp は受け取らない
→ つまり XR の世界と時間軸が別
その結果…
useFrame(state, delta) が XR ではズレる理由
R3F の delta は R3F の clock 基準。
XR の delta は XRSession の timestamp 基準。
比較してみると:
| 項目 | R3F | WebXR |
|---|---|---|
| 基準 | React / R3F の clock | XR デバイスの内部クロック |
| FPS | ほぼ 60Hz | Quest は 72 / 90 / 120Hz |
| 更新タイミング | ブラウザ rAF 依存 | XR デバイス依存 |
| 処理順 | useFrame → gl.render | XR pose → controller → gl.render |
→ 完全に別軸で動き続ける。
そのため XR 内で:
- アニメーションが遅れる
- VRM の足が地滑りする
- コントローラーの反応がワンテンポ遅れる
- カメラがワンフレーム“後追い”になる
- VR 空間の動きに微細な酔いが発生する
という XR 特有の問題が起きる。
結論:R3F のループと XR のループは「構造的に同期しない」
これはバグではない。
R3F と WebXR は、別の目的で作られた2つのループが “偶然出会ってしまった” 結果。
3. “R3F useFrame が XR と同期しない” 問題
R3F の useFrame() は本来、
R3F が持つ独自のレンダリングループに完全依存している。
しかし XR に入った瞬間、
Three.js は gl.setAnimationLoop() を XR 専用のループへ切り替えるため、
R3F のループと XR のループが別周波数で動作する。
XR と R3F のフレームレートは一致しない
XR デバイス(例:Quest2/3)は、 デバイス固有のフレームレートで駆動する。
| モード | フレームレート |
|---|---|
| Quest 2 | 72Hz / 90Hz |
| Quest 3 | 90Hz / 120Hz |
一方、R3F のループは ブラウザ側の rAF (60Hz 前後) に依存しているため、 XR に入っても“勝手に 60Hz 付近で回り続ける”。
これによって delta(前フレームとの差分時間)が噛み合わない。
結論1:R3F の delta が XR と一致しない
R3F の delta は 1/60 秒付近で進むが、 XR の delta は 1/72・1/90・1/120 秒で進む。
結果:
- useFrame の世界は 60Hz
- XR の世界は 72〜120Hz
という“2つの別世界”が生まれる。
結論2:動きが“1フレーム遅れて反映”される
React Three Fiber では、 すべての状態更新(カメラ・物体移動・VRMの骨更新)が useFrame の流れに乗るため、
XR 側の pose 更新(72〜120Hz) ↓ useFrame の反応(60Hz)
となり、常に 1 フレーム遅れて反映される。
これが:
- ジャダー(細かいブレ)
- 遅延した視点移動
- カメラのカクつき
- モデルの僅かな“痙攣”
を発生させる。
結論3:VRM / MMD のリグがズレる理由もこれ
VRM や MMD は アニメーションの姿勢がフレーム単位で厳密。 しかし、useFrame が XR と同期していないため:
- hips(腰)が地滑りする
- 足 IK が一瞬遅れて反応する
- モーションのリズムがズレる
- 手・腕が XR カメラの位置に追従しない
- 走りモーションが“常に 1 フレーム後ろ”になる
こういった XR 特有の “違和感” が生じる。
結論4:XR 空間では酔いやすくなる
VR の酔いは 「視点移動の同期ズレ」 が最大の原因。 R3F useFrame と XR が別ループで動く以上、 このズレは構造的に発生する。
- 首の回転 → 実描画まで数ms遅れる
- カメラのマトリクス更新が1フレーム遅延
- 動き出しと見た目のズレが脳に負担
特に 90Hz/120Hz の高速レンダリングでは 60Hz の遅れがより顕著に見える。
まとめ:R3F useFrame は「XR の世界線」に届いていない
R3F の useFrame は:
- 60Hz
- R3F 独自の clock
- React のループ基準
- WebXR の timestamp 不使用
という “XR 非対応のタイムライン” で動く。
そのため XR 内では:
- 動作の遅延
- モデルのズレ
- VRM の足の同期崩れ
- コントローラ反応の鈍さ
- 軽い酔い
が発生する。
これは 実装ミスではなく構造的問題。
4. 解決策1:frameloop=“never” + setAnimationLoop() を自前で奪う
R3F と XR のループが同期しない最大の原因は、 R3F が自前の render loop を持っていることだ。
これを避ける最も根本的な方法が、
R3F のループを“完全に止めて”、XR のループに従わせる
という設計である。
Step1:R3F の内部ループを完全停止(frameloop=“never”)
R3F の <Canvas> には
frameloop という設定がある。
<Canvas frameloop="never">
これにより R3F は:
useFrame()を自動では呼ばないgl.render()を自動では呼ばないadvance()を自動では実行しない
つまり R3F の「自前ループ」が丸ごと停止する。
R3F の render loop が止まるということは、 React の state 更新 → useFrame → render の連鎖が勝手に走らなくなる。
これが XR 統合の前提になる。
Step2:XR の animationLoop 内で R3F を手動で advance する
Three.js は XR モードになると
gl.setAnimationLoop(loop)
に制御を移す。 ここに R3F の root.advance() を挿入するのがポイント。
例:
gl.setAnimationLoop((timestamp, frame) => {
// --- XR の世界 ---
// XR の head pose / controllers を更新
// frame.getViewerPose() などの処理
// --- R3F の世界 ---
root.advance(timestamp / 1000); // ★ R3F の更新を XR 時間に合わせる
});
R3F に渡す時間は XR の timestamp をそのまま使うことで、 R3F 内部の clock と XR 内の clock が 完全に統一される。
効果:R3F と XR のループが「1本化」される
これで実現するのは:
XR が全体のループ主導権を握る
R3F は XR が支配する時間軸(timestamp)で advance される
useFrame の delta が XR と一致する
カメラ・VRM・アニメーションが XR に同期する
1フレーム遅れが消える
つまり R3F と XR が同じ “世界線” に立つ。
これが最も美しい統合方法。
具体的メリット
● 1. VRM / MMD のアニメーションが XR と一致する
VRM の hips が地滑りする問題が改善される。
● 2. カメラの動きが XR と完全同期
酔いが減り、姿勢の遅延がなくなる。
● 3. useFrame の挙動が XR の FPS と一致
120Hz の世界でも正しく動く。
● 4. 反射・水面・キャラクターの制御が安定
特に root.advance を XR ループに乗せることで すべてのフレーム処理が XR の“1本化”される。
副作用(ここが重要)
この方式は強力だが、いくつか注意点もある。
1. React 再レンダリングの頻度に注意
advance() は「R3F の world 更新」だけであり React コンポーネントの再描画は直接関係ない。
React 側の再レンダが増えると XR の 90Hz/120Hz にボトルネックを作る。
2. Suspense は XR 中に再マウントを引き起こす
ループを XR に統一しても React の再マウントは XR の描画を乱す点は変わらない。
(これが後の章で扱う“小さなブラックアウト現象”につながる。)
3. R3F の invalidate も使わなくなる
frameloop=“never” では invalidate() を呼んでも R3F の自動ループは動かない。
XR の animationLoop が唯一の心臓となる。
結論
この方法は、R3F × XR の統合として もっとも根本的で、もっとも強力で、もっとも安定する。
「R3F の世界を XR の世界線に合わせることで、 useFrame・アニメ・カメラが完全に同期する」
という状態を実現できる。
5. 解決策2:useFrame の “priority” を使って XR 更新を最前に置く
frameloop="never" や setAnimationLoop() を使わず、
R3F のデフォルトループのまま XR と少しでも同期させたい場合に使えるのが
useFrame の priority(優先順位)
である。
R3F の useFrame は “優先度付きのコールスタック”
R3F の useFrame() は、以下のように priority(数字) を指定できる。
useFrame(
(state, delta) => {
// XR に合わせたい更新処理
},
1 // ★ priority(数字が大きいほど先に実行される)
);
デフォルトの priority は 0。 数字が大きいほど 早い段階で実行される。
これにより、XR に関係する処理を最前にまとめられる
たとえば次のような処理を priority 1 にすれば、 R3F の通常レンダリングより“前”で実行される:
- HMD(head pose)の追従
- コントローラー位置の更新
- VRM の hips・root の位置合わせ
- XR 空間内のアバターの補正
結果として、R3F の描画が XR の状態を後追いしにくくなる。
priority を使うメリット(軽減できるズレ)
◎ 1. コントローラーの“操作遅延”が減る
XRInputSource の位置や回転を R3F 描画より前に反映できる。
◎ 2. VRM の腰・足が少し安定する
まったく同期できないわけではなく、 “見た目の遅れ”が少しだけ目立たなくなる。
◎ 3. 視点(head pose)のズレが若干緩和
R3F が持つ 60Hz の遅延を “描画前に先に計算する”という形で隠せる。
結果として、 「XR内の違和感が 30〜50% 程度は軽減される」 という効果がある。
しかし、この方式は「完全な同期」ではない
priority をいくら調整しても、 根本的に R3F と XR のループは別物である。
R3F は 60Hz の世界
XR は 72 / 90 / 120Hz の世界
両者の delta は一致しない
R3F の clock は XR timestamp を知らない
useFrame の順序を変えても“loop 自体は別”
つまり…
priority は“順番”の調整であって、 “ループの統一”ではない。
構造的問題は残る。
なぜ完全解決にならないのか?
priority 方式では、
- R3F の clock は 60Hz
- XR の clock は 72〜120Hz
- useFrame は R3F のクロックに従う
- WebXR の animationLoop は XR デバイス側で進む
という関係が 絶対に変わらない。
そのため、
・優先順位は変わる → でも“ズレは残る”
・遅延は少し減る → でも“完全同期しない”
・酔いは減る → でも“完全には消えない”
という、「軽減」止まりの対策である。
まとめ:priority は “小手先の最適化”、Loop統一ほどの効果はない
priority 方式はこうまとめられる:
- 実装が簡単
- 一部のズレが減る
- useFrame の順序が整う
- 表面的な遅延が軽減される
- ✖ しかし根本的な同期にはならない
- ✖ VRM/MMD の完全同期は不可能
- ✖ XR の timestamp は利用されない
したがって:
短期的な応急処置にはなるが、
XR で本格運用するには不十分。
6. XRControllerModelFactory が R3F と相性最悪な理由
React Three Fiber(R3F)は、Three.js の SceneGraph を 「React の state 管理」+「差分レンダリング(reconciliation)」 の思想で包み込んだフレームワークである。
つまり R3F は、 SceneGraph のオブジェクトを「React が管理する存在」として扱う。
React が管理する、ということは:
- props → SceneGraph へ反映
- 再レンダ時に差分チェック
- 不整合があれば reconciler が修正
- unmount 時に破棄
という React 流のライフサイクルが必ず適用される。
しかし XRControllerModelFactory は“React 無視”で動く
XRControllerModelFactory(Three.js 純正)は、 XR コントローラーのモデルを Raw Three.js の世界 で勝手に構築し、
- 子オブジェクトの追加
- transform の変更
- ツリーの再構築
- マテリアルの差し替え
- pose に応じて更新
などを 直接 mutate(破壊的操作) する。
つまり:
Three.js の生の SceneGraph を、 React の知らないところで勝手に書き換える。
React の世界観と真っ向から衝突する
React / R3F は本来こう考えている:
「SceneGraph の状態は全部 React の管理下にある」
しかし XRControllerModelFactory は:
「いや、俺が Three.js 直で書き換える」
この結果、
- React が “正しい状態” だと思っている SceneGraph
- XRControllerModelFactory が“実際に書き換えた” SceneGraph
この2つがズレる。
具体的に起きる破綻(XR で出る不具合の正体)
1. コントローラーモデルが突然 “消える”
R3F 再レンダ時に、 「React が持っている SceneGraph」と一致しないため、 reconciler が “存在しない子要素扱い” して破棄する。
2. モデルが “1フレーム遅れて反応する”
React の reconcile → Three.js の mutate という 二重更新 が発生する。
3. コントローラーの位置がズレ続ける
R3F の useFrame のタイミングと XRControllerModelFactory のタイミングが別軸で進むため、 常に時間差が出る。
4. マテリアルが突然リセットされる
reconciler が“差分を修正”しようとするため、 XRControllerModelFactory が設定したマテリアルが上書きされる。
5. 特定の環境で「手が地面に落ちる」
モデルの transform が React と Three.js によって 上書き合戦になり、 ワールド座標が狂う。
この問題は R3F の構造上“修正不能”
重要なのは:
R3F と XRControllerModelFactory は設計思想が正反対 ・R3F:差分レンダリング(reconciler) ・XR系:破壊的 mutate(imperative)
このギャップは調整で治せる類の問題ではない。
安定させるための正しい使い方(推奨パターン)
R3F と XRControllerModelFactory を共存させるためには、 “React 管理に入れない” という方針が必要になる。
1. コントローラーモデルを R3F 管理にしない
// React コンポーネントにしない
<primitive object={controllerModel} />
これはダメ。 React が “勝手に触っていいもの” と認識してしまう。
2. Three.js の raw object として直接 add する
// XRSession 開始時などで
scene.add( controllerGrip );
ここではっきり 「このオブジェクトは React の管理外」 に置く。
3. Suspense や re-mount に巻き込まない
Suspense の fallback 切り替えが起きると SceneGraph の一部が再構築されるため、
- コントローラーモデルが消える
- XR pose 更新が初期化される
- カメラが 0,0,0 に戻る
などが発生する。
したがって:
<Suspense fallback={null}>
{/* XR コントローラーを含めない */}
</Suspense>
は必須。
まとめ:XRControllerModelFactory は R3F の“最凶の衝突ポイント”
- XR コントローラーモデルは Three.js が破壊的に mutate
- R3F は React の思想で差分更新
- その2つは根本的に相容れない
- 結果:消える / ズレる / 反応しない / 崩壊 が起きる
- React 管理に入れず “raw Three.js object” として扱うのが唯一の安定策
7. Suspense が XR 内だと危険な理由
React の <Suspense> は、本来 Web UI を滑らかにするための仕組みだ。
・コンポーネントがロード中 ・非同期ローダーで遅延 ・fallback の切り替え ・再レンダー / 再マウントによるレイアウト再計算
これらを “自動的に置き換える” メカニズムを持つ。
しかし、この 「一部のコンポーネントを再マウントする」 という性質が、XR に入った瞬間、致命的な不具合を起こす。
XR 内の SceneGraph は「絶対に再構築してはならない」
WebXR の世界では、
- HMD の head pose
- controller grip pose
- viewer pose(視点)
- reference space(floor / local-floor)
- projection / view matrix
- input source state
これらすべてが 「継続性」 を前提に更新される。
つまり:
1フレーム前と “同じオブジェクト” を更新し続けることが前提の世界。
ところが React Suspense は “コンポーネントのツリーを一度破壊して新しいものに置き換える”。
→ これが XR session と全く噛み合わない。
Suspense が XR 中に起こす「壊滅的な現象」
ポーズの再計算がズレる
再マウント時、head pose / controller pose が “初期状態” になる。
- カメラの位置が 0,0,0 に戻る
- コントローラーの gripSpace の姿勢が再計算
- 直前のフレームと整合性が消える
XR が必要とする「前フレームとの継続性」が崩壊する。
カメラが 0,0,0 に飛ぶ
R3F が生成し直した camera や scene が XRSession の viewerPose と一致しないため、
- XR が “初期姿勢” で再アタッチ
- その瞬間に camera が 0,0,0 に瞬間移動
- 利用者視点では “真っ黒” や “床の中” が一瞬表示される
手が地面に埋まる
controller グリップが再マウントされると:
- XR が把握する gripSpace(コントローラーの空間座標)
- R3F 内のコントローラーモデルの transform
が 一致しなくなる。
その結果:
- グリップだけ床に落ちる
- モデルだけ数十cmズレる
- 次のフレームで急に正位置に戻る → “ガクッ” と落ちる挙動になる
画面が“瞬間ブラックアウト”する
Suspense の fallback が発動すると SceneGraph が瞬時に変わる。
XRSession は「見ていた SceneGraph が突然消える」ため、 XR compositor がセーフティとして 1フレーム黒を挿入する。
これがユーザー視点では “チラつき” / “ブラックアウト” に見える。
なぜ XR では re-mount が絶対に許されないのか?
理由は明確で、
WebXR は “連続フレーム” が前提の API だから。
- headPose
- inputSource
- viewerPose
- referenceSpace
- anchors
- hitTestSource
いずれも 「前フレーム → 次フレーム」 の Δ を取っている。
つまり一度でも SceneGraph が破壊(unmount)されると:
- XR がトラッキングロスト扱い
- pose の履歴がリセット
- index の参照が崩壊
- XR compositor が fallback を発動
こうして XR 特有の破綻が起きる。
だから XR の Suspense は「非推奨」ではなく「構造的に使えない」
まとめると:
再マウントは XR の pose と一致しなくなる
fallback が SceneGraph を破壊する
コンポーネントのライフサイクルが XR と噛み合わない
R3F の reconciler が XR の状態を知らない
head pose / controller pose が初期化される
ブラックアウトが起きる
R3F × XR では、 Suspense は UI の便利機能ではなく“破壊装置”になる。
実際の推奨パターン
XR 内では Suspense を使わない
ローダーは XR セッション開始前に完了させる
遅延読み込みの内容は XR 空間に入れない
fallback={null} は強制的 re-mount を発生させるので NG
コントローラーモデルは Suspense の外で raw Three.js 管理
結論:XR セッション中に “Scene を書き換える React の仕組み” はすべて危険
Suspense はその代表例であり、 XR × R3F の最大の地雷のひとつ。
8. 何が最も重くなるのか? R3F × XR のパフォーマンス問題
R3F は「UIフレームワークの哲学」を Three.js に持ち込んでいるため、 WebXR の 90〜120Hz 世界では、構造的に負荷が跳ね上がる。
ここでは XR 時に特有のボトルネックを正確に列挙する。
1. Scene graph の “diff 計算” が XR では致命的に重い
R3F の本質は 「React の reconciler(差分更新)」。
React と同じく、
- “前フレームのツリー”
- “今フレームのツリー”
を比較し、
どのノードに変化があったか?
どの子を差し替えるべきか?
どの props が変わったか?
これらを 毎フレーム行う。
🟥 VR はフレームレートが高すぎて破綻
通常 Web は 60fps 前提だが、XR は:
- 72Hz(Quest2)
- 80Hz(Quest3 デフォルト)
- 90Hz(PCVR)
- 120Hz(ハイエンド)
→ 差分計算が単純に 1.5〜2倍の頻度で走る。
これは Three.js(命令型更新)には存在しないコスト。 R3F だけが背負う“UI式コスト”である。
2. useFrame の多重呼び出し問題(最大の地獄)
R3F は useFrame() を登録した数だけ “毎フレーム呼び出す”。
たとえば:
- カメラ制御
- コントローラー更新
- VRM アニメ
- postprocessing
- minimap
- UI overlay
- physics
- エフェクト
これらが 10〜20 個あると、
90Hz × 20 = 1800 callbacks/秒
120Hz × 20 = 2400 callbacks/秒
🟥 useFrame の数が多いほど指数的に重くなる
React の仕組みの上に Three.js を乗せているため、 フレーム単位での関数コールが非常に多い。
Three.js ネイティブでは、 単純に自分で必要な update を1回呼ぶだけなのでこの問題は存在しない。
3. XR Input の更新が “二重で走る” バグ構造
XR のコントローラーや HPD pose は本来、
Three.js 側(XRManager)が XRSession の timestamp に同期して更新する。
ところが R3F を併用すると:
Three.js 側:XRManager が更新
R3F 側:useFrame() が独自に “遅れて”更新
つまり、
同じ入力が 2回更新される。
このとき:
- ボタンイベント
- gripPose / targetRayPose
- controller の transform
- inputSource の更新
これらが R3F の delta / clock と XRSession の timestamp の差によってズレて呼ばれる。
結果:
- 手がガクッと揺れる
- ボタンが 2回押された扱いになる
- ポインターがカクつく
- VRM の腕がわずかにブレ続ける
などの症状が起きる。
4. React の「再レンダー」が XR の 120Hz と相性最悪
React はコンポーネントの state が変わると ツリーの一部を再レンダーする。
これが XR では:
- 120fps中に再レンダーが挟まる
- SceneGraph の一部が再構築される
- その瞬間「XR compositor が1フレ黒を差し込む」
結果:
→ カクつき / ブラックアウト / pose リセット Suspense と同じ地雷がふつうの再レンダーでも起きる。
5. Canvas の内部 clock が XR timestamp と一致しない
R3F の clock は
performance.now()
WebXR の timestamp は
XR timestamp (より正確・専用の値)
この差によって:
- VRM のリグが “足ずれ” を起こす
- 物理演算の delta が安定しない
- カメラのスムースモーションが破綻する
まとめ:R3F × XR が重くなる理由は「React 由来の負荷」+「XR との二重更新」
🟥 1. SceneGraph の diff → 120Hz だと暴力的な負荷
🟥 2. useFrame の多重コール → 1000〜3000回/秒のコール
🟥 3. XR input の二重更新 → “ブレ”“遅延”の温床
🟥 4. React の再レンダーが XR compositor に刺さる
🟥 5. clock が XR timestamp と合わずアニメがズレる
結論:R3F は「通常の Three.js より重い」のではなく
XR で“構造的に Two-loop(2つのループ)を抱えてしまう”
だから XR では弱い。
💬 コメント