注意点・パフォーマンス
本番投入前に必ず読み返すページ。Three.js は強力だが、「何もしないと自動で最適化されない」類の ライブラリなので、知っておくべき罠と最適化テクが多い。
メモリリーク(dispose の鉄則)
Three.js のGPU リソースは GC されない。明示的に dispose() を呼ぶこと。
dispose が必要なもの
geometry.dispose()material.dispose()texture.dispose()renderTarget.dispose()renderer.dispose()
シーン全体を破棄するヘルパー
function disposeNode(o) {
if (o.geometry) o.geometry.dispose()
if (o.material) {
const mats = Array.isArray(o.material) ? o.material : [o.material]
for (const m of mats) {
for (const k of Object.keys(m)) {
const v = m[k]
if (v && v.isTexture) v.dispose()
}
m.dispose()
}
}
}
function disposeAll(root) {
root.traverse(disposeNode)
}
// SPA でシーンを切り替えるとき
disposeAll(scene)
renderer.dispose()
renderer.domElement.remove()
scene.remove(mesh) しただけでは GPU バッファは残る。
外すのとdispose するの両方が必要。順番は dispose → remove でも remove → dispose でも良い(参照が他にあると dispose しても完全には消えない)。
共有して使い回す
ジオメトリ・マテリアル・テクスチャは複数の Mesh で共有可能。同じ箱を 100 個出すなら、 ジオメトリとマテリアルは 1 つだけ作って共有する。
const geom = new THREE.BoxGeometry()
const mat = new THREE.MeshStandardMaterial({ color: "#7dd3fc" })
for (let i = 0; i < 100; i++) {
const m = new THREE.Mesh(geom, mat) // 共有
m.position.x = i * 0.5
scene.add(m)
}
Instancing — 大量配置の決定版
100 個の箱を別々の Mesh にすると、ドローコール 100 回。
InstancedMesh なら1回。
const N = 1000
const geom = new THREE.BoxGeometry()
const mat = new THREE.MeshStandardMaterial()
const im = new THREE.InstancedMesh(geom, mat, N)
const tmp = new THREE.Object3D()
for (let i = 0; i < N; 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()
im.setMatrixAt(i, tmp.matrix)
}
im.instanceMatrix.needsUpdate = true
scene.add(im)
個別の色を持たせたい時は im.setColorAt(i, color) + im.instanceColor.needsUpdate = true。
BatchedMesh(新しめ)
異なるジオメトリを 1 つのドローコールで描く新機能(Three.js r163+)。 形が違うけど一括で描きたいシーンに有効。
LOD(Level of Detail)
距離に応じて簡略モデルへ切り替える。
const lod = new THREE.LOD()
lod.addLevel(highDetailMesh, 0) // 0 m から
lod.addLevel(midDetailMesh, 10) // 10 m から
lod.addLevel(lowDetailMesh, 30) // 30 m から
scene.add(lod)
// カメラ距離は自動で判定される
Frustum culling
カメラに映っていないオブジェクトはレンダラが自動でスキップする。 ただしバウンディングが正しくないとスキップしない / 逆にスキップされて消えることがある。
- 頂点を動かすシェーダ(
onBeforeCompileで頂点シフト)→mesh.frustumCulled = false - カスタム BufferGeometry は
computeBoundingSphere()を必ず呼ぶ
レンダラ設定での節約
- DPR を上限 2 にクランプ:
renderer.setPixelRatio(Math.min(devicePixelRatio, 2)) - antialias を切る: 重い時は MSAA を切って FXAA(ポストプロセス)に切り替え
- shadow map サイズ: 1024 か 2048 で十分。4096 は 1 枚で 64MB
- 静的シーンは shadow を焼く:
renderer.shadowMap.autoUpdate = false
静的シーンは1回だけ render
変化がないシーンは毎フレーム描かない。必要な時だけ render() を呼ぶ。
function invalidate() {
renderer.render(scene, camera)
}
// カメラを動かすたびに
controls.addEventListener("change", invalidate)
window.addEventListener("resize", () => {
// ...
invalidate()
})
// 初回
invalidate()
毎フレームのアロケーションを避ける
// BAD: 毎フレーム new
function update() {
const v = new THREE.Vector3(...)
mesh.position.copy(v)
}
// GOOD: 使い回す
const tmp = new THREE.Vector3()
function update() {
tmp.set(...)
mesh.position.copy(tmp)
}
SSR(Next.js / Astro / Nuxt)
Three.js は window / document を参照する。SSR でサーバ側でロードされるとクラッシュする。
- Next.js (App Router): ファイル先頭に
"use client" - Next.js (Pages Router):
dynamic(() => import("./Scene"), { ssr: false }) - Astro:
client:only="react"ディレクティブ - Nuxt:
<ClientOnly>でラップ
カラースペース問題
- r152 以降は sRGB がデフォルト。古いチュートリアルどおりに書くと色がおかしい。
- カラーテクスチャ(
map/emissiveMap)にはtexture.colorSpace = THREE.SRGBColorSpace - データテクスチャ(
normalMap等)はそのまま(NoColorSpace) - ポストプロセス使う時は最後に
OutputPassを入れて tonemap + sRGB 変換
Z-fighting(深度のチラつき)
2つの面がほぼ同じ深度にあると、フレームごとに前後が入れ替わってチラつく現象。
- near / far の比を小さく。
near = 0.001のような極端な値は避ける。 - 同じ平面上に置くなら少しオフセット(
position.y += 0.001)。 polygonOffsetプロパティを使う(polygonOffsetFactor/polygonOffsetUnits)。- 背景はプレートではなく
scene.backgroundや Skybox に。
影のチラつき(shadow acne)
自分の影で表面が縞模様になる現象。
light.shadow.bias = -0.0005あたりで調整light.shadow.normalBias = 0.02- shadow map の解像度を上げる
透明物の重なり
透明オブジェクトは奥から手前へ手動で並べるのが基本(深度書き込みしないため)。
renderer.sortObjects = true(既定)で自動ソートはされる- 大きな半透明面の重なり(パーティクル等)は
depthWrite: false - 順序に厳しい場合は
renderOrderを明示
WebGL コンテキストロスト
ブラウザの裏で別のWebGLコンテキストが立ち上がったり、GPU が忙しいとWebGL がロストする。 Three.js は復旧をサポートするが、自前リソースは作り直しが必要。
renderer.domElement.addEventListener("webglcontextlost", (e) => {
e.preventDefault()
// レンダーループを止めるなど
})
renderer.domElement.addEventListener("webglcontextrestored", () => {
// リソース再作成
})
ブラウザサポート
- WebGL2 が前提。WebGL1 のサポートはr163 以降は基本的に切られている。
- WebGPU バックエンドは
three/webgpuから。Chromium は安定、Safari は対応進行中。 - iOS Safari は WebGL の挙動に独自クセが多い。実機で確認。
計測ツール
- stats.js — FPS / メモリ表示
- three-perf — ドローコール・三角形数・テクスチャ数
- Spector.js(ブラウザ拡張) — フレーム毎の WebGL コール一覧
- Chrome DevTools の Performance タブ — long task や JS の重い処理
- WebGL Inspector / RenderDoc — 上級デバッグ
バージョンアップへの備え
- Three.js はマイナーバージョンでも破壊的変更が普通。リリースノートを毎回確認。
- r152 の色管理切替、r156 の
encoding→colorSpace、r163 のBatchedMesh等が直近の大きな節目。 - 本番ではバージョンを固定(
"three": "0.170.0"のように)。アップデートはまとまった時間を取って。
判断フロー
- 静的シーンなら → 1回 render で済ませる
- 同じ形が大量にあるなら → InstancedMesh / BatchedMesh
- カクつくなら → DPR を下げる、shadow map を縮める、antialias を切る
- 初回が重いなら → KTX2 / Draco / Meshopt 圧縮 + Loading 表示
- raycast が重いなら → 対象配列を絞る、layers で分ける、不要 mesh は
raycast = () => {} - SPA で遷移するなら → 必ず
disposeAll+renderer.dispose
手を動かすなら Three.js Manual を1周、
深く学ぶなら Three.js Journey が定番。
サンプルは公式の examples が圧倒的に多く、
ソースコードもブラウザから読めるので、近い表現を探してコードを覗くのが最短。