パフォーマンス・注意点

R3F は薄いラッパなので、パフォーマンスもほぼ Three.js のセオリー通り。 だが「React と Three の境目」で起きる固有の罠もある。本番投入前に通読しておく。

パフォーマンスの基本方針

  1. useFrame の中で setState しない。ref を直接 mutate。
  2. 毎フレームのアロケーションを避けるVector3/Matrix4useRef で使い回す。
  3. 必要なときだけレンダリングframeloop="demand" + invalidate()
  4. 同一メッシュは instancing。1000 個の木は instancedMesh
  5. 当たり判定を不要なものから外すraycast={() => null}
  6. シャドウは静的なら焼くBakeShadows / AccumulativeShadows
  7. オブジェクトを共有する。geometry / material / texture を React の外で 1 回だけ作る。

frameloop = "demand"(オンデマンドレンダリング)

静的なシーン(カメラ動かしたとき・状態変化時にだけ描き直せばいい)なら、 frameloop="demand" にすると必要な瞬間だけ描画するようになる。

<Canvas frameloop="demand">
  <OrbitControls /> {/* 自動で invalidate を呼んでくれる */}
  <Model />
</Canvas>

外部から再描画したい時は invalidate() を呼ぶ:

const invalidate = useThree(s => s.invalidate)

useEffect(() => {
  // 状態が変わったら次のフレームで描き直す
  invalidate()
}, [someState])
うまくハマると劇的

UI 中心のページに 3D を埋め込んでいるケースは、frameloop="demand"CPU/GPU 使用率が桁違いに下がる。バッテリ消費も改善する。

Instancing — 大量のオブジェクト

同じジオメトリ・マテリアルを 1000 個並べるなら絶対に instancing。 ドローコールが 1000 → 1 になる。

function Forest({ count = 1000 }) {
  const ref = useRef<THREE.InstancedMesh>(null!)
  const tmp = useMemo(() => new THREE.Object3D(), [])

  useEffect(() => {
    for (let i = 0; i < count; i++) {
      tmp.position.set(
        (Math.random() - 0.5) * 50,
        0,
        (Math.random() - 0.5) * 50,
      )
      tmp.rotation.y = Math.random() * Math.PI * 2
      tmp.scale.setScalar(0.5 + Math.random())
      tmp.updateMatrix()
      ref.current.setMatrixAt(i, tmp.matrix)
    }
    ref.current.instanceMatrix.needsUpdate = true
  }, [count, tmp])

  return (
    <instancedMesh ref={ref} args={[undefined, undefined, count]}>
      <coneGeometry args={[0.5, 1.5, 8]} />
      <meshStandardMaterial color="#22c55e" />
    </instancedMesh>
  )
}

宣言的に書きたい時は drei の <Instances> + <Instance>

BatchedMesh(新しめ)

異なるジオメトリを 1 つのドローコールで描く新機能(Three.js r163+)。 形が違うけど一括で描きたいシーンに有効。

Suspense とアセットロード

useLoader / useGLTF は Suspense 境界で待ち合わせる。境界を Canvas 内に置くのがコツ。

import { Suspense } from "react"

<Canvas>
  <Suspense fallback={null}>
    <Model />
    <Environment preset="city" />
  </Suspense>
</Canvas>

グローバルなロード進捗を出したいなら drei の <Loader /> を Canvas のに置く。

import { Loader } from "@react-three/drei"

<Canvas>...</Canvas>
<Loader />

DPR(DevicePixelRatio)

Retina 画面で 100% 描くと負荷が4倍。dpr={[1, 2]} のようにclampする。

<Canvas dpr={[1, 1.5]}>...</Canvas>

動的 DPR(drei の AdaptiveDpr)

FPS 低下時に DPR を下げて回復させる仕組み。ユーザーがマウス動かしてる間だけ DPR 下げる等の運用。

import { AdaptiveDpr, AdaptiveEvents, PerformanceMonitor } from "@react-three/drei"

<Canvas>
  <AdaptiveDpr pixelated />
  <AdaptiveEvents />
  ...
</Canvas>

カラースペースとトーンマッピング

Strict Mode(React 18+)

Strict Mode はマウント時に effect を 2 回走らせる。imperative にリソースを作る effectがリークする可能性がある。

dispose — メモリリーク対策

R3F はJSX から外れた Object3D を自動で dispose() する(geometry / material / texture も)。 だが共有しているリソースで自動 dispose されると困る場合は明示的に止める。

{/* このメッシュが unmount してもジオメトリ/マテリアルは捨てない */}
<mesh dispose={null}>
  <primitive object={sharedGeometry} attach="geometry" />
  <primitive object={sharedMaterial} attach="material" />
</mesh>

SSR — Next.js / Astro 等

Canvas はブラウザ専用。SSR されると window 等が無くてエラーになる。 Canvas を含むコンポーネントはクライアント専用にする

Next.js (App Router)
"use client"
import { Canvas } from "@react-three/fiber"
// このファイルから export するコンポーネントはクライアント専用
Next.js (Pages Router) でも動的 import
import dynamic from "next/dynamic"
const Scene = dynamic(() => import("./Scene"), { ssr: false })

R3F でやりがちなアンチパターン

1. 毎フレーム new する

// BAD: 毎フレーム Vector3 を作る
useFrame((state) => {
  const v = new THREE.Vector3(state.pointer.x, state.pointer.y, 0)
  v.unproject(state.camera)
  ref.current.position.copy(v)
})

// GOOD: useRef で再利用
const v = useRef(new THREE.Vector3()).current
useFrame((state) => {
  v.set(state.pointer.x, state.pointer.y, 0).unproject(state.camera)
  ref.current.position.copy(v)
})

2. useFrame で setState

// BAD: 毎フレーム再レンダー → 即死
const [pos, setPos] = useState([0, 0, 0])
useFrame(() => setPos([pos[0] + 0.01, 0, 0]))

// GOOD: ref を直接 mutate
const ref = useRef<THREE.Mesh>(null!)
useFrame((_, dt) => { ref.current.position.x += dt })

3. JSX 内でオブジェクトリテラル

// BAD: 毎レンダーで配列が新規生成 → R3F が「変わった」と判定して set する
<mesh position={[0, 1, 0]} />

// 多くの場合は実害ないが、ホットパスでは:
const POS = useMemo(() => [0, 1, 0] as const, [])
<mesh position={POS} />

実用上は最初の書き方で十分速い。気になる箇所だけ useMemo でいい。

4. args を頻繁に変える

args 変更はインスタンス再生成。 変えたい値は positionscale 等のフィールドで。args は固定値で持つ。

5. 同じ primitive object を2箇所

Three.js のオブジェクトは1つの親にしか属せない。同じ scene を2箇所に <primitive> すると壊れる。
対処: drei の <Clone> または scene.clone()

6. Canvas の中で hooks を取り出す

Canvas のコンポーネントから useFrame / useThree を呼ぶとエラー。 間違いやすい:

// BAD
function App() {
  const { camera } = useThree()  // Canvas の外なのでエラー
  return <Canvas> ... </Canvas>
}

// GOOD
function App() {
  return <Canvas><Inner /></Canvas>
}
function Inner() {
  const { camera } = useThree()  // OK
  return null
}

「mesh が見えない」チェックリスト

  1. カメラの位置と向き: メッシュがフラスタム外
  2. ライトがない: Standard/Physical Material は黒くなる → ambientLight を入れる
  3. scale が 0: 初期値が 0 のまま
  4. visible: false: 親で制御していないか
  5. 背面カリング: 板を裏から見ている → side={THREE.DoubleSide}
  6. near/far から外れる: カメラ範囲を確認
  7. z-fighting で消える: 重なった面でちらつく
  8. opacity 0 / transparent: 完全透明
  9. Canvas が 0 サイズ: 親に height が無い

計測ツール

r3f-perf の例

npm install r3f-perf
import { Perf } from "r3f-perf"

<Canvas>
  <Perf position="top-left" />
  ...
</Canvas>

テクスチャ・ジオメトリの使い回し

コンポーネントの内部で useMemo で作るより、モジュールレベルで一回作って共有する方が軽い:

// モジュールトップで一度だけ
const sharedGeom = new THREE.BoxGeometry()
const sharedMat = new THREE.MeshStandardMaterial({ color: "#7dd3fc" })

function Boxes() {
  return (
    <>
      {Array.from({ length: 100 }).map((_, i) => (
        <mesh key={i} position={[i * 0.5, 0, 0]} dispose={null}>
          <primitive object={sharedGeom} attach="geometry" />
          <primitive object={sharedMat} attach="material" />
        </mesh>
      ))}
    </>
  )
}

判断フロー

  1. 動かないオブジェクトばかりなら → frameloop="demand"
  2. 同じ形が大量にあるなら → instancedMesh
  3. カクつくなら → dpr を下げる、shadow map を縮める、antialias を切る
  4. 初回が重いなら → useGLTF.preload + <Preload all /> + <Loader /> で目隠し
  5. raycast が重いなら → 不要オブジェクトに raycast={() => null}
  6. 影だけが重いなら → 静的影は焼く(BakeShadows
  7. マテリアルが多すぎるなら → 共有 / Merged で減らす
困ったら

R3F のドキュメント(r3f.docs.pmnd.rs)と drei のドキュメント(drei.docs.pmnd.rs)が公式情報源。
実装例は pmndrs/drei のソースを直接読むのがいちばん早いことも多い。
Discord Poimandres も活発。