パフォーマンス・注意点
R3F は薄いラッパなので、パフォーマンスもほぼ Three.js のセオリー通り。 だが「React と Three の境目」で起きる固有の罠もある。本番投入前に通読しておく。
パフォーマンスの基本方針
- useFrame の中で
setStateしない。ref を直接 mutate。 - 毎フレームのアロケーションを避ける。
Vector3/Matrix4はuseRefで使い回す。 - 必要なときだけレンダリング。
frameloop="demand"+invalidate()。 - 同一メッシュは instancing。1000 個の木は
instancedMesh。 - 当たり判定を不要なものから外す。
raycast={() => null}。 - シャドウは静的なら焼く。
BakeShadows/AccumulativeShadows。 - オブジェクトを共有する。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>
カラースペースとトーンマッピング
- R3F のデフォルトは sRGB + ACES Filmic Tone Mapping。近代的な見た目になる。
- 古い見た目(Three.js 旧デフォルト)にしたいなら
linear flatを Canvas に渡す。 - テクスチャの色がおかしいときは、
map.colorSpace = THREE.SRGBColorSpaceを確認。
Strict Mode(React 18+)
Strict Mode はマウント時に effect を 2 回走らせる。imperative にリソースを作る effectがリークする可能性がある。
- R3F 自身は Strict Mode 対応している。
- 自分で書く
useEffectは cleanup を必ず書く(特にnewしたインスタンスのdispose())。 useLoader/ drei のuseGLTFはキャッシュ前提なので大丈夫。
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 を含むコンポーネントはクライアント専用にする。
"use client"
import { Canvas } from "@react-three/fiber"
// このファイルから export するコンポーネントはクライアント専用
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 変更はインスタンス再生成。
変えたい値は position や scale 等のフィールドで。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 が見えない」チェックリスト
- カメラの位置と向き: メッシュがフラスタム外
- ライトがない: Standard/Physical Material は黒くなる → ambientLight を入れる
- scale が 0: 初期値が 0 のまま
- visible: false: 親で制御していないか
- 背面カリング: 板を裏から見ている →
side={THREE.DoubleSide} - near/far から外れる: カメラ範囲を確認
- z-fighting で消える: 重なった面でちらつく
- opacity 0 / transparent: 完全透明
- Canvas が 0 サイズ: 親に
heightが無い
計測ツール
- drei の
<Stats />— FPS / メモリ。簡易だが速い。 r3f-perf— R3F 専用の詳細パネル。draw call / geometries / textures 数等。- Chrome DevTools の Performance タブ — long task や GPU 使用率を確認。
- three-perf(ブラウザ拡張) — three.js の draw call・三角形数。
- Spector.js — フレーム毎の WebGL コール一覧(重い WebGL 命令の特定)。
- React DevTools Profiler — 不必要な再レンダリングの検出。
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>
))}
</>
)
}
判断フロー
- 動かないオブジェクトばかりなら →
frameloop="demand" - 同じ形が大量にあるなら →
instancedMesh - カクつくなら →
dprを下げる、shadow map を縮める、antialias を切る - 初回が重いなら →
useGLTF.preload+<Preload all />+<Loader />で目隠し - raycast が重いなら → 不要オブジェクトに
raycast={() => null} - 影だけが重いなら → 静的影は焼く(
BakeShadows) - マテリアルが多すぎるなら → 共有 / Merged で減らす
R3F のドキュメント(r3f.docs.pmnd.rs)と
drei のドキュメント(drei.docs.pmnd.rs)が公式情報源。
実装例は pmndrs/drei のソースを直接読むのがいちばん早いことも多い。
Discord Poimandres も活発。