Three.jsベストプラクティス100選(2026年版)

ルカム・ジョスラン

ルカム・ジョスラン

代表取締役、Utsubo株式会社

2026年1月12日·29分で読めます
Three.jsベストプラクティス100選(2026年版)

Table of Contents

ウェブで高パフォーマンスな3D体験を構築するには、APIの知識だけでは不十分です。ブラウザ、GPU、JavaScriptがどのように連携し、どこにボトルネックが潜んでいるかを理解する必要があります。

本記事では、Three.js開発における100の実践的なベストプラクティスをまとめました。新しいWebGPUレンダラーに重点を置きながら、最適化の全範囲をカバーしています。既存プロジェクトの最適化でも、新規開発でも、これらのヒントがより速く、スムーズな体験の実現に役立ちます。

対象読者: Three.jsを使用するウェブ開発者で、パフォーマンスとコード品質を向上させたい方。これから始める方にはThree.js Journeyをお勧めします。


重要ポイント

  • WebGPUは本番環境対応済み—r171以降、ゼロコンフィグでインポート可能。WebGL 2への自動フォールバック付き
  • ドローコールがパフォーマンスの鍵—フレームあたり100未満を目標に
  • インスタンシングとバッチングでドローコールを90%以上削減可能
  • 不要なリソースはすべてdispose—ジオメトリ、マテリアル、テクスチャ、レンダーターゲット
  • TSL(Three Shader Language)が未来—一度書けばWebGPUでもWebGLでも動作
  • 可能な限りベイク—ライトマップ、シャドウ、アンビエントオクルージョン
  • 最適化の前にプロファイル—stats-gl、renderer.info、Spector.jsを活用

WebGPUレンダラー

WebGPUレンダラーは、Three.jsのグラフィックス処理における根本的な変革です。2025年9月にSafari 26がサポートを開始して以降、すべての主要ブラウザでWebGPUを使用できるようになりました。変更点の詳細はThree.js 2026年の変化をご覧ください。

1. ゼロコンフィグのWebGPUインポートと非同期初期化を使用

r171以降、WebGPUの導入は簡単ですが、非同期初期化が必要です:

import { WebGPURenderer } from 'three/webgpu';

const renderer = new WebGPURenderer();
await renderer.init(); // 最初のレンダリング前に必須

function animate() {
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}
animate();

init()呼び出しは必須です—GPUアダプターとデバイスをリクエストします。これがないと、レンダリングは静かに失敗します。バンドラー設定やポリフィルは不要です。

2. 自動WebGL 2フォールバックを信頼する

ブラウザがWebGPUをサポートしていない場合、WebGPURendererは自動的にWebGL 2にフォールバックします。別々のコードパスは不要—1つのレンダラーを出荷すれば、Three.jsが互換性を処理します。

3. TSL(Three Shader Language)を習得する

TSLはThree.jsのノードベースマテリアルシステムで、WGSL(WebGPU)またはGLSL(WebGL)にコンパイルされます。シェーダーコードを2回書く代わりに、TSLで一度だけ書きます:

import { color, positionLocal, sin, time } from 'three/tsl';

const material = new MeshStandardNodeMaterial();
material.colorNode = color(1, 0, 0).mul(sin(time).mul(0.5).add(0.5));

TSLはカスタムシェーダーの推奨アプローチです。

4. パーティクルシステムをコンピュートシェーダーに移行

CPUベースのパーティクル更新は一般的なハードウェアで5万パーティクル程度でボトルネックに達します。WebGPUコンピュートシェーダーなら数百万パーティクルも処理可能です—Three.js WebGPUパーティクルサンプルを参照:

import { instancedArray, storage, uniform } from 'three/tsl';

const positions = instancedArray(particleCount, 'vec3');
const velocities = instancedArray(particleCount, 'vec3');

5. instancedArrayでGPU永続バッファを使用

instancedArrayはフレーム間で持続するGPUバッファを作成します。従来のパーティクルシステムでパフォーマンスを低下させていたCPU-GPU間のデータ転送を排除できます。

6. パフォーマンス限界に達したらWebGPUに移行

現在のWebGLプロジェクトがスムーズに動作しているなら、急いで移行する必要はありません。以下の場合に移行を検討してください:

  • ドローコールが多いシーンでフレームが落ちる
  • 物理シミュレーション用のコンピュートシェーダーが必要
  • 複雑なポストプロセッシングでカクつく

7. ブラウザサポートマトリックスを把握する

ブラウザWebGPUサポート
Chrome/Edgev113以降(2023年)
Firefoxv141以降(Windows)、v145以降(macOS ARM)
Safariv26以降(2025年9月)

すべての主要ブラウザがWebGPUをサポートしています—待ちは終わりました。(出典:caniuse.com/webgpu

8. forceWebGLを戦略的に使用

forceWebGL: trueオプションはWebGPURendererでWebGLモードを強制します。これは以下の場合に有用です:

  • WebGPU対応マシンでWebGLフォールバック動作をテスト
  • バックエンド間のシェーダーコンパイル差異をデバッグ
  • WebGPUでまだ利用できない特定のWebGL拡張をサポート

本番環境でWebGL専用ビルドの場合は、バンドルサイズを小さくするためにWebGLRendererを直接使用することを検討してください。

9. 特定シナリオで2-10倍のパフォーマンス向上を期待

WebGPUが威力を発揮するのは:

  • ドローコールが多いシーン(数百のオブジェクト)
  • 計算集約型エフェクト(パーティクル、物理)
  • 複雑なシェーダーパイプライン

これらの改善はChromeのWebGPUベンチマークと私たちのExpo 2025での本番環境経験で実証されています。普遍的に高速というわけではありません—具体的なユースケースでプロファイルしてください。

10. ノードマテリアルで動的カスタマイズ

ノードマテリアルはpositionNodecolorNodenormalNodeなどのプロパティを受け取ります:

const material = new MeshStandardNodeMaterial();
material.positionNode = positionLocal.add(displacement);
material.colorNode = vertexColor;

WebGLではカスタムシェーダーが必要だったエフェクトを実現できます。

11. 計算負荷の高いシーンにはrenderAsyncを使用

シーンにコンピュートシェーダーが含まれる場合、非同期レンダリングを使用してGPU作業を適切に同期します:

async function animate() {
  await renderer.renderAsync(scene, camera);
  requestAnimationFrame(animate);
}

これにより、依存するレンダーパスが始まる前にコンピュートパスが完了することが保証されます。コンピュートのないシンプルなシーンでは、通常のrender()で問題ありません。

12. WebGPUのバインディングモデルを理解する

WebGPUはWebGLの個別バインディングとは異なり、リソースをバインドグループにバッチ処理します。このアーキテクチャは以下を優遇します:

  • 頻繁に更新されるuniform(時間、カメラなど)を1つのバインドグループにまとめる
  • 静的データ(テクスチャ、マテリアル)は別のグループに配置
  • ドローコール間のバインドグループ切り替えを最小化

Three.jsはこれを自動的に処理しますが、パフォーマンスデバッグ時に理解しておくと役立ちます。

13. 読み書き可能なコンピュートにはストレージテクスチャを使用

通常のテクスチャと異なり、ストレージテクスチャはコンピュートシェーダーで読み書き両方が可能です:

import { storageTexture, textureStore, uvec2 } from 'three/tsl';

const outputTexture = new StorageTexture(width, height);
const store = textureStore(outputTexture, uvec2(x, y), computedColor);

流体シミュレーション、画像処理、GPU駆動レンダリングなどのエフェクトに不可欠です。

14. WebGPU機能検出を適切に処理

すべてのWebGPU機能が普遍的に利用可能とは限りません。使用前に確認してください:

const adapter = await navigator.gpu?.requestAdapter();
if (!adapter) {
  // WebGLにフォールバックまたはエラーを表示
  return;
}

// 特定の機能を確認
const hasFloat32Filtering = adapter.features.has('float32-filterable');
const hasTimestamps = adapter.features.has('timestamp-query');

15. Chrome WebGPU DevToolsでデバッグ

ChromeのGPUデバッグ(chrome://gpu)はWebGPUのステータスとエラーを表示します。より深いデバッグには:

  1. chrome://flagsで「WebGPU Developer Features」を有効化
  2. PerformanceパネルでGPU作業をトレース
  3. コンソールでシェーダーコンパイルエラーを確認—WebGLより詳細です

バリデーションエラーは、問題のある呼び出しを指すスタックトレースと共にコンソールに表示されます。

16. フレームごとのバッファ更新を最小化

WebGPUバッファ書き込みは高コストです。多くの小さなバッファを更新する代わりに:

// 悪い例:複数の小さな更新
particles.forEach(p => p.buffer.update());

// 良い例:単一のバッチ更新
const data = new Float32Array(particles.length * 4);
particles.forEach((p, i) => data.set(p.data, i * 4));
batchBuffer.update(data);

毎フレーム更新されるパーティクルデータにはinstancedArrayを使用してください。

17. 物理シミュレーションにコンピュートシェーダーを使用

パーティクル以外にも、コンピュートシェーダーは物理シミュレーションに優れています:

import { compute, instancedArray } from 'three/tsl';

const positions = instancedArray(count, 'vec3');
const velocities = instancedArray(count, 'vec3');

const physicsCompute = compute(() => {
  const pos = positions.element(instanceIndex);
  const vel = velocities.element(instanceIndex);
  // 力の適用、衝突検出、制約
  positions.element(instanceIndex).assign(pos.add(vel.mul(deltaTime)));
});

renderer.compute(physicsCompute);

18. コンピュートシェーダーで地形を生成

GPU上でのプロシージャル地形生成により、リアルタイム編集と大規模スケールが可能になります:

const heightmap = storageTexture(resolution, resolution);

const terrainCompute = compute(() => {
  const uv = uvec2(instanceIndex.mod(resolution), instanceIndex.div(resolution));
  const height = mx_noise_float(uv.mul(scale)).mul(amplitude);
  textureStore(heightmap, uv, vec4(height, 0, 0, 1));
});

19. ワークグループ共有メモリを活用

スレッド間でデータ共有が必要なコンピュートシェーダーには、ワークグループ変数を使用:

import { workgroupArray, workgroupBarrier } from 'three/tsl';

const sharedData = workgroupArray('float', 256);
// 共有メモリにデータをロード
sharedData.element(localIndex).assign(inputData);
workgroupBarrier(); // 全スレッドを同期
// これで全スレッドがsharedDataから読み取り可能

共有メモリは繰り返しアクセスパターンでグローバルメモリより10-100倍高速です。

20. GPU駆動レンダリングにインダイレクトドローを使用

コンピュートシェーダー出力に基づいてGPUに何をレンダリングするか決定させます:

const drawIndirectBuffer = new IndirectStorageBufferAttribute(4, 'uint');

// コンピュートシェーダーが入力: [vertexCount, instanceCount, firstVertex, firstInstance]
const cullCompute = compute(() => {
  // GPU上でフラスタムカリング、LOD選択
  if (visible) drawIndirectBuffer.element(1).atomicAdd(1);
});

mesh.drawIndirect = drawIndirectBuffer;

フレームごとのGPUカリングで数百万のインスタンスをレンダリングするのに不可欠です。


アセット最適化

3Dアセットは最大のパフォーマンスボトルネックになりがちです。50MBのGLTFファイルは、レンダリングコードがどれだけ最適化されていても読み込み時間を破壊します。

21. Dracoでジオメトリを圧縮

Draco圧縮はジオメトリのファイルサイズを90-95%削減します(出典:gltf-transform docs):

gltf-transform draco model.glb compressed.glb --method edgebreaker

解凍はWeb Workerで行われるため、メインスレッドをブロックしません。

22. テクスチャ圧縮にKTX2を使用

PNGやJPEGテクスチャはGPUメモリで完全に展開されます。200KBのPNGがVRAMで20MB以上を占有することも。KTX2とBasis Universalは、GPUで圧縮されたまま維持され、メモリ使用量を約10分の1に削減します:

gltf-transform uastc model.glb optimized.glb

23. 品質重視ならUASTC、サイズ重視ならETC1S

  • UASTC: 高品質、大きいファイル。法線マップや主要テクスチャに最適
  • ETC1S: 小さいファイル、許容できる品質。環境テクスチャやセカンダリアセットに最適

法線マップにはUASTC、ディフューズにはETC1Sが基本ルールです。

24. gltf-transform CLIをマスターする

gltf-transformはGLTF最適化のスイスアーミーナイフです:

# フル最適化パイプライン
gltf-transform optimize model.glb output.glb \
  --texture-compress ktx2 \
  --compress draco

25. Shopifyのgltf-compressorで視覚比較

gltf-compressorを使えば、変更をプレビューしながらインタラクティブにテクスチャを圧縮できます。「C」キーを押すとオリジナル、離すと圧縮版を表示。「品質が悪く見え始めるまでどこまで圧縮できるか」がわかります。

26. LOD(Level of Detail)を実装

距離に応じてハイポリモデルをローポリバージョンに切り替えます。React Three FiberではDreiの<Detailed />が便利です:

<Detailed distances={[0, 50, 100]}>
  <HighPolyModel />
  <MediumPolyModel />
  <LowPolyModel />
</Detailed>

LODにより大規模シーンでフレームレートが30-40%向上することがあります。

27. テクスチャをアトラス化してバインドを削減

複数テクスチャ = 複数テクスチャバインド = レンダリング遅延。テクスチャをアトラスに統合し、UV座標を更新してください。モバイルGPUでは特にオーバーヘッドが大きく軽減されます。

28. デコーダーパスを正しく設定

DracoとKTX2にはデコーダーが必要です。一度セットアップしてください:

import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';

const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');

const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath('/basis/');

デコーダーファイルはCDNでホストして高速アクセスを確保してください。

29. Dracoの代替としてMeshoptを検討

MeshoptはDracoと同等の圧縮率を、より高速な解凍で提供します。gzipと組み合わせればDracoの比率に匹敵し、クライアント側の負荷が軽くなります。両方テストしてユースケースに合わせて判断してください。


ドローコール最適化

シーン内の各メッシュは通常1つのドローコールを生成します。各ドローコールにはCPUオーバーヘッドがあります。重要な洞察:三角形数よりドローコール数が重要

30. フレームあたり100未満のドローコールを目標に

これが黄金律です。100ドローコール未満なら、ほとんどのデバイスでスムーズな60fpsを維持できます。500を超えると、強力なGPUでも苦戦します。renderer.info.render.callsで確認してください。

31. 繰り返しオブジェクトにはInstancedMeshを使用

1,000本の木を個別メッシュでレンダリング = 1,000ドローコール。InstancedMeshを使えば = 1ドローコール:

const mesh = new InstancedMesh(geometry, material, 1000);
for (let i = 0; i < 1000; i++) {
  matrix.setPosition(positions[i]);
  mesh.setMatrixAt(i, matrix);
}

ある不動産デモでは、椅子をインスタンスレンダリングに切り替えることでドローコールを9,000から300に削減しました。

32. 異なるジオメトリにはBatchedMeshを使用

BatchedMesh(r156以降)は、同じマテリアルを共有する複数のジオメトリを単一のドローコールに統合します。InstancedMeshと異なり、各インスタンスが異なるジオメトリを持てます。

33. メッシュ間でマテリアルを共有

Three.jsは同一マテリアルのメッシュをバッチ処理します。オブジェクトごとに新しいマテリアルを作成すると、この最適化が機能しません:

// 悪い例:メッシュごとに新しいマテリアル
meshes.forEach(m => m.material = new MeshStandardMaterial({ color: 'red' }));

// 良い例:マテリアルを共有
const sharedMaterial = new MeshStandardMaterial({ color: 'red' });
meshes.forEach(m => m.material = sharedMaterial);

34. BufferGeometryUtilsで静的ジオメトリをマージ

静的シーンでは、読み込み時にメッシュをマージします:

import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';

const merged = mergeGeometries([geo1, geo2, geo3]);
const mesh = new Mesh(merged, sharedMaterial);

複数の代わりに1つのドローコールで済みます。

35. モダンブラウザではアレイテクスチャを使用

アレイテクスチャは複数のテクスチャをレイヤーに統合し、シェーダーでインデックスアクセスします。BatchedMeshと組み合わせることで、最小限のドローコールで多様な外観を実現できます。

36. フラスタムカリングを理解する

Three.jsはカメラのビュー外のオブジェクトを自動的にカリングします—ドローコールを生成しません。この動作は制御できます:

// デフォルト:ビュー外のオブジェクトはカリング
mesh.frustumCulled = true;

// 常にレンダリングすべきオブジェクトでは無効化(スカイボックス、パーティクルシステム)
skybox.frustumCulled = false;

// 複雑なロジックで手動カリング:
const frustum = new Frustum();
const matrix = new Matrix4().multiplyMatrices(
  camera.projectionMatrix,
  camera.matrixWorldInverse
);
frustum.setFromProjectionMatrix(matrix);

if (frustum.intersectsObject(mesh)) {
  // オブジェクトは可視
}

フラスタムカリングは無料の最適化です—正しく機能するためにバウンディングボックスが正確であることを確認してください。


メモリ管理

Three.jsはGPUリソースを自動的にガベージコレクトしません。使用が終わったジオメトリ、マテリアル、テクスチャは明示的にdisposeする必要があります。

37. 完了時にすべてのGPUリソースをdispose

Three.jsはGPUリソースをガベージコレクトしません。ジオメトリ、マテリアル、テクスチャを常にdisposeしてください:

function cleanupMesh(mesh) {
  mesh.geometry.dispose();

  if (Array.isArray(mesh.material)) {
    mesh.material.forEach(mat => {
      Object.values(mat).forEach(prop => {
        if (prop?.isTexture) prop.dispose();
      });
      mat.dispose();
    });
  } else {
    Object.values(mesh.material).forEach(prop => {
      if (prop?.isTexture) prop.dispose();
    });
    mesh.material.dispose();
  }

  scene.remove(mesh);
}

単一の4Kテクスチャが64MB以上のVRAMを使用します。ジオメトリとシェーダープログラムも持続します。renderer.info.memoryを監視してください—カウントが増え続ける場合、リークがあります。

38. GLTFからのImageBitmapテクスチャを特別に処理

GLTFテクスチャはImageBitmapとして読み込まれ、明示的なクローズが必要です:

texture.source.data.close?.();
texture.dispose();

close()がないと、ImageBitmapオブジェクトがリークします。

39. スポーンされるエンティティにオブジェクトプーリングを使用

頻繁に作成・破棄されるオブジェクト(弾丸、パーティクル、敵)には、新規作成ではなくプールを使用。アロケーションオーバーヘッドとGCポーズを回避できます:

class ObjectPool {
  constructor(factory, reset, initialSize = 20) {
    this.factory = factory;
    this.reset = reset;
    this.pool = [];

    // プールを事前準備
    for (let i = 0; i < initialSize; i++) {
      const obj = factory();
      obj.visible = false;
      this.pool.push(obj);
    }
  }

  acquire() {
    const obj = this.pool.pop() || this.factory();
    obj.visible = true;
    return obj;
  }

  release(obj) {
    this.reset(obj);
    obj.visible = false;
    this.pool.push(obj);
  }
}

// 使用例
const bulletPool = new ObjectPool(
  () => new Mesh(bulletGeometry, bulletMaterial),
  (bullet) => bullet.position.set(0, 0, 0),
  50
);

// スポーン
const bullet = bulletPool.acquire();
scene.add(bullet);

// デスポーン
bulletPool.release(bullet);

ローディング中にプールを事前準備して、ランタイムでのアロケーションスパイクを回避してください。

40. テクスチャをキャッシュして再利用

各テクスチャを一度だけ読み込み、どこでも参照:

const textureCache = new Map();

function getTexture(url) {
  if (!textureCache.has(url)) {
    textureCache.set(url, textureLoader.load(url));
  }
  return textureCache.get(url);
}

41. レンダーターゲットもdispose

ポストプロセッシングのレンダーターゲットもdisposeが必要です:

renderTarget.dispose();

各レンダーターゲットはフレームバッファメモリを確保します。

42. コンポーネントのアンマウント時にクリーンアップ(React)

React Three Fiberでは、クリーンアップ関数を使用:

useEffect(() => {
  return () => {
    geometry.dispose();
    material.dispose();
    texture.dispose();
  };
}, []);

シェーダーとマテリアル

シェーダー最適化は、初心者と上級者を分ける領域です。小さな変更で2倍のパフォーマンス向上が得られることもあります。特にモバイルで顕著です。

43. モバイルではmediump精度を使用

モバイルGPUはmediumpをhighpの約2倍の速度で処理します:

precision mediump float;

highpが必要なのは深度計算や位置計算など特定の場合のみです。

44. varying変数を最小化

varyingは頂点シェーダーとフラグメントシェーダー間でデータを転送します。モバイルGPUでは3未満に抑えてください:

// 悪い例:varyingが多い
varying vec3 vPosition;
varying vec3 vNormal;
varying vec2 vUv;
varying vec3 vWorldPosition;
varying vec4 vColor;

// 良い例:データをパック
varying vec4 vData1; // xy = uv, zw = packed normal
varying vec4 vData2; // xyz = position, w = unused

45. 条件分岐をmix()とstep()に置き換え

分岐はGPU並列性を損ないます:

// 悪い例:分岐
if (value > 0.5) {
  color = colorA;
} else {
  color = colorB;
}

// 良い例:ブランチレス
color = mix(colorB, colorA, step(0.5, value));

46. RGBAチャンネルにデータをパック

テクセルあたり1つではなく4つの値を格納:

vec4 data = texture2D(dataTex, uv);
float value1 = data.r;
float value2 = data.g;
float value3 = data.b;
float value4 = data.a;

テクスチャフェッチが75%削減されます。

47. 動的ループを避ける

動的境界のループは最適化を妨げます:

// 悪い例:動的
for (int i = 0; i < count; i++) { ... }

// 良い例:固定
for (int i = 0; i < 16; i++) { ... }

または短いループは完全に展開してください。

48. 生のGLSL/WGSLよりTSLを優先

TSLはクロスコンパイル、uniform、attributeを自動処理します。生のシェーダーでは2つのコードベース(GLSL + WGSL)の保守が必要です。

49. ノードマテリアルでカスタムエフェクトを構築

ノードマテリアルは合成可能です:

const noise = mx_noise_float(positionLocal);
const displaced = positionLocal.add(normalLocal.mul(noise));
material.positionNode = displaced;

50. FnでTSL関数を再利用可能に作成

Fnパターンで再利用可能なシェーダーロジックを作成:

import { Fn, float, vec3 } from 'three/tsl';

const fresnel = Fn(([normal, viewDir, power]) => {
  const dotNV = normal.dot(viewDir).saturate();
  return float(1).sub(dotNV).pow(power);
});

// 使用
material.emissiveNode = fresnel(normalWorld, viewDirection, 3.0).mul(color);

関数は一度コンパイルされ、マテリアル間で再利用できます。

51. TSL組み込みノイズ関数を使用

TSLにはMaterialXノイズ関数が含まれています—外部ライブラリは不要:

import { mx_noise_float, mx_noise_vec3, mx_fractal_noise_float } from 'three/tsl';

// シンプルなノイズ
const n = mx_noise_float(positionLocal.mul(scale));

// オクターブ付きフラクタルノイズ
const fbm = mx_fractal_noise_float(positionLocal, octaves, lacunarity, gain);

// カラーバリエーション用3Dノイズ
const colorNoise = mx_noise_vec3(uv.mul(10));

52. シェーダープログラムを再利用

Three.jsは同一シェーダーのプログラムを再利用します。uniformを同じ方法で定義すれば、プログラムは共有されます。不要なバリエーションはプログラムの増殖を引き起こします。


ライティングとシャドウ

ライティングは高コストです。シャドウはさらに高コストです。シャドウ付きのリアルタイムライティングは、他のすべてを合わせたよりも多くのGPU時間を消費する可能性があります。

53. アクティブライトを3つ以下に制限

追加のライトごとに計算複雑性が増加します。3ライトを超える場合は、ベイクまたは環境マップの使用を検討してください。

54. PointLightシャドウのコストを理解

PointLightシャドウは6回のシャドウマップレンダリングが必要です(キューブの各面に1回):

ドローコール = オブジェクト × 6 × ポイントライト数

シャドウ付き2つのPointLightで10オブジェクト = 120の追加ドローコール。

55. 静的シーンではライトマップをベイク

ライティングが変わらない場合、テクスチャにベイク:

ベイクされたライティングはレンダリング時にほぼ無料です。

56. 大規模シーンにはカスケードシャドウマップを使用

CSMはカメラ近くで高品質、遠くで低品質のシャドウを提供:

import { CSM } from 'three/addons/csm/CSM.js';

const csm = new CSM({
  maxFar: camera.far,
  cascades: 4, // デスクトップ: 4, モバイル: 2
  shadowMapSize: 2048
});

57. シャドウマップサイズを適切に設定

  • モバイル: 512-1024
  • デスクトップ: 1024-2048
  • 品質重視: 4096

大きなシャドウマップはメモリを二次的に消費します。

58. @react-three/lightmapでランタイムベイク

読み込み時にライトマップを生成し、ある程度のライトカスタマイズを可能に:

import { Lightmap } from '@react-three/lightmap';

<Lightmap>
  <Scene />
</Lightmap>

59. アンビエントライトに環境マップを使用

環境マップ(HDRI)はライトごとの計算なしでリアルなライティングを提供:

const envMap = pmremGenerator.fromScene(scene).texture;
scene.environment = envMap;

60. シャドウカメラのフラスタムを調整

タイトなフラスタムはシャドウ品質を向上:

directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;

デフォルトを使わず、シーンに合わせてください。

61. 静的シーンではシャドウの自動更新を無効化

ライトやシャドウキャストオブジェクトが動かない場合、シャドウマップの自動更新を無効化:

renderer.shadowMap.autoUpdate = false;

// ライトやオブジェクトが動いたときは手動で更新をトリガー:
renderer.shadowMap.needsUpdate = true;

毎フレームのシャドウパスを節約できます。動きが時々ある場合は、必要なときだけシャドウを更新してください。

62. シンプルなケースにはフェイクシャドウを使用

ラジアルグラデーションの半透明プレーンでコンタクトシャドウを安価にフェイクできます。リアルシャドウのコストなしで十分な場合も多いです。


React Three Fiber

React Three Fiber(R3F)はReactのメンタルモデルをThree.jsに追加します。同時に、Reactのレンダリングパラダイム特有のパフォーマンスの落とし穴も追加されます。

63. useFrame内で変更、setStateは使わない

核心ルール:Three.jsの変更はuseFrame内で行い、Reactステートは使わない:

// 悪い例:Reactの再レンダリングをトリガー
const [rotation, setRotation] = useState(0);
useFrame(() => setRotation(r => r + 0.01));

// 良い例:直接変更
const meshRef = useRef();
useFrame(() => {
  meshRef.current.rotation.x += 0.01;
});

64. 静的シーンにはframeloop="demand"を使用

何もアニメーションしない場合、毎フレームレンダリングしない:

<Canvas frameloop="demand">
  <Scene />
</Canvas>

モバイルデバイスのバッテリーを節約できます。

65. 手動更新にはinvalidate()を呼び出す

オンデマンドレンダリングでは、必要なときに再レンダリングをトリガー:

const invalidate = useThree(state => state.invalidate);

// 変更後に
invalidate();

66. useFrame内でオブジェクトを作成しない

オブジェクト作成はガベージコレクションをトリガー:

// 悪い例:毎フレーム新しいVector3を作成
useFrame(() => {
  mesh.position.copy(new Vector3(1, 2, 3));
});

// 良い例:再利用
const targetPos = useMemo(() => new Vector3(1, 2, 3), []);
useFrame(() => {
  mesh.position.copy(targetPos);
});

67. フレームレート独立にはdeltaを使用

デバイスによってリフレッシュレートが異なります:

useFrame((state, delta) => {
  // 悪い例:速度がフレームレートで変わる
  mesh.rotation.x += 0.1;

  // 良い例:一定の速度
  mesh.rotation.x += delta * speed;
});

68. LODにはDreiのを使用

ボイラープレートなしのLOD:

import { Detailed } from '@react-three/drei';

<Detailed distances={[0, 20, 50]}>
  <HighDetail />
  <MediumDetail />
  <LowDetail />
</Detailed>

69. useGLTF.preloadでモデルをプリロード

必要になる前にモデルを読み込む:

useGLTF.preload('/model.glb');

// 後でコンポーネント内で
const { scene } = useGLTF('/model.glb');

70. 重いコンポーネントはReact.memoでラップ

不要な再レンダリングを防止:

const ExpensiveModel = React.memo(({ url }) => {
  const { scene } = useGLTF(url);
  return <primitive object={scene} />;
});

71. 再マウントではなく表示/非表示を切り替え

再マウントはバッファの再作成とシェーダーの再コンパイルを伴います:

// 悪い例:アンマウント/マウント
{showModel && <Model />}

// 良い例:表示切り替え
<Model visible={showModel} />

72. r3f-perfでモニタリング

R3F用のドロップインパフォーマンスモニタリング:

import { Perf } from 'r3f-perf';

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

ポストプロセッシングとエフェクト

ポストプロセッシングはレンダリングされたシーンに追加のGPUパスを実行します。各エフェクトにコストがありますが、賢い設定で影響を最小化できます。

73. Three.jsデフォルトよりpmndrs/postprocessingを使用

pmndrsポストプロセッシングライブラリはエフェクトを自動的にマージしてパス数を削減:

import { EffectComposer, Bloom, Vignette } from 'postprocessing';

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(new EffectPass(camera, new Bloom(), new Vignette()));

74. ポストプロセッシング用にレンダラーを設定

EffectComposer使用時の最適設定:

// WebGL
const renderer = new WebGLRenderer({
  powerPreference: 'high-performance',
  antialias: false,      // AAはポストプロセッシングで処理
  stencil: false,
  depth: false
});

// WebGPU
const renderer = new WebGPURenderer({
  antialias: false,
  powerPreference: 'high-performance'
});
await renderer.init();

WebGPUは深度/ステンシルバッファを自動処理します。ポストプロセッシングでSMAA/FXAAを追加する場合、両レンダラーでネイティブAAを無効にすると効果的です。

75. パフォーマンスのためにマルチサンプリングを無効化

不要な場合:

<EffectComposer multisampling={0}>
  <Bloom />
</EffectComposer>

76. トーンマッピングはパイプラインの最後に

ポストプロセッシングでは、レンダラーのトーンマッピングを無効化:

renderer.toneMapping = NoToneMapping;

代わりにToneMappingEffectを最後のエフェクトとして追加。

77. セレクティブブルームを実装

すべてをブルームさせる必要はありません。レイヤーまたはしきい値を使用:

const bloom = new SelectiveBloomEffect(scene, camera, {
  luminanceThreshold: 0.9,
  luminanceSmoothing: 0.3
});

78. 最後にアンチエイリアシングを追加

ポストプロセッシングはWebGL組み込みのAAをバイパスします。SMAAまたはFXAAを最終パスとして追加:

composer.addPass(new EffectPass(camera, new SMAAEffect()));

79. ブルームパラメータを慎重に調整

  • intensity: 全体の強度(通常0.5-2.0)
  • luminanceThreshold: ブルームする最小輝度(0.8-1.0)
  • radius: 広がりサイズ(0.5-1.0)

低解像度のブルームは安価で、見栄えも良いことが多いです。

80. 解像度と品質のトレードオフを検討

半解像度でレンダリングしてアップスケールすれば、フレームレートが2倍に:

composer.setSize(window.innerWidth / 2, window.innerHeight / 2);

81. 互換性のあるエフェクトをマージ

一部のエフェクトはシェーダーパスを統合できます:

// 複数エフェクトを単一パスで
const effects = new EffectPass(camera, bloom, vignette, chromaticAberration);

82. WebGPU用にはThree.jsネイティブポストプロセッシングを使用

WebGPUプロジェクトでは、pmndrs/postprocessingの代わりにThree.js組み込みのTSLノードベースポストプロセッシングを使用:

import { pass, bloom, fxaa } from 'three/tsl';

const postProcessing = new PostProcessing(renderer);
const scenePass = pass(scene, camera);
postProcessing.outputNode = scenePass.pipe(bloom()).pipe(fxaa());

pmndrsライブラリはWebGLプロジェクトに引き続き優れていますが、TSLベースのポストプロセッシングはフルコンピュートシェーダーサポートを持つWebGPUのネイティブソリューションです。


ローディングとCore Web Vitals

重い3D体験は、注意しないとCore Web Vitalsを破壊する可能性があります。リッチな体験を提供しながら良好なLCP、FID/INP、CLSを維持する方法を紹介します。

83. ファーストビュー外の3DコンテンツはLazy Load

3Dがすぐに見えない場合は、読み込みを遅延:

const observer = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    loadThreeJsScene();
    observer.disconnect();
  }
});

observer.observe(canvasContainer);

84. Three.jsモジュールをコード分割

すべてを最初からバンドルしない:

const Three = await import('three');
const { GLTFLoader } = await import('three/addons/loaders/GLTFLoader.js');

85. 重要なアセットをプリロード

ファーストビューの3Dには積極的にプリロード:

<link rel="preload" href="/model.glb" as="fetch" crossorigin>
<link rel="preload" href="/texture.ktx2" as="fetch" crossorigin>

86. プログレッシブローディングを実装

低解像度を最初に表示し、高解像度をバックグラウンドでロード:

// 低解像度を即座にロード
const lowRes = await loadModel('low.glb');
scene.add(lowRes);

// 高解像度を非同期でロード
loadModel('high.glb').then(highRes => {
  scene.remove(lowRes);
  scene.add(highRes);
});

87. 重い処理をWeb Workerにオフロード

物理、プロシージャル生成、アセット処理はメインスレッド外で実行可能:

const worker = new Worker('/physics-worker.js');
worker.postMessage({ positions, velocities });

88. 大規模シーンをストリーミング

巨大な環境では、セクションを動的にロード:

function updateVisibleChunks(cameraPosition) {
  const visibleChunks = getChunksNear(cameraPosition);
  visibleChunks.forEach(chunk => {
    if (!chunk.loaded) loadChunk(chunk);
  });
}

89. ロード中はプレースホルダージオメトリを使用

何かをすぐに表示:

// ロード中はシンプルなボックス
const placeholder = new Mesh(
  new BoxGeometry(1, 1, 1),
  new MeshBasicMaterial({ color: 0x808080, wireframe: true })
);
scene.add(placeholder);

// ロード完了後に置き換え
loadModel().then(model => {
  scene.remove(placeholder);
  scene.add(model);
});

90. R3FでSuspenseを使用

R3FはReact Suspenseと統合:

<Suspense fallback={<Loader />}>
  <Model />
</Suspense>

開発とデバッグ

最高の最適化は、問題を早期に発見して不要になる最適化です。これらのツールとテクニックは、問題が本番環境での問題になる前に特定するのに役立ちます。

91. stats-glでWebGL/WebGPUモニタリング

stats-glはリアルタイムのFPS、CPU、GPUメトリクスを提供します。WebGLとWebGPUの両方で動作します:

import Stats from 'stats-gl';

const stats = new Stats();
document.body.appendChild(stats.dom);

function animate() {
  stats.begin();
  // ... render
  stats.end();
  requestAnimationFrame(animate);
}

92. lil-guiでライブ調整をセットアップ

lil-guiはあらゆるJavaScriptオブジェクトのデバッグパネルを作成:

import GUI from 'lil-gui';

const gui = new GUI();
gui.add(camera.position, 'x', -10, 10);
gui.add(camera.position, 'y', -10, 10);
gui.add(light, 'intensity', 0, 2);

開発中に正しい値を見つけるために必須です。

93. Spector.jsでプロファイル

Spector.jsはWebGLフレームをキャプチャするブラウザ拡張機能です。すべてのドローコール、テクスチャバインド、シェーダープログラムを確認できます。実際に何が起こっているかを理解するのに非常に役立ちます。

94. renderer.infoを定期的にチェック

setInterval(() => {
  console.log('Calls:', renderer.info.render.calls);
  console.log('Triangles:', renderer.info.render.triangles);
  console.log('Geometries:', renderer.info.memory.geometries);
  console.log('Textures:', renderer.info.memory.textures);
}, 1000);

これらの数値を監視してください。安定して、増加しないはずです。

95. three-mesh-bvhで高速レイキャスティング

three-mesh-bvhは8万ポリゴン以上に対して60fpsでレイキャスティングを可能に:

import { MeshBVH, acceleratedRaycast } from 'three-mesh-bvh';

mesh.geometry.boundsTree = new MeshBVH(mesh.geometry);
mesh.raycast = acceleratedRaycast;

複雑なジオメトリを持つインタラクティブシーンには必須です。

96. ブラウザDevToolsのPerformanceタブを使用

Chrome/Edge DevToolsは時間がどこで使われているかを表示:

  • 長いフレーム
  • ガベージコレクションの一時停止
  • ブロッキングJavaScript

合成テストだけでなく、実際のセッションをプロファイルしてください。

97. 機能検出付きでGPUタイミングクエリを使用

WebGPUタイムスタンプクエリにはtimestamp-query機能が必要で、デフォルトでは有効になっていません:

// 機能サポートを確認
const adapter = await navigator.gpu.requestAdapter();
const hasTimestamps = adapter.features.has('timestamp-query');

if (hasTimestamps) {
  const device = await adapter.requestDevice({
    requiredFeatures: ['timestamp-query']
  });
  // タイムスタンプクエリセットを作成可能
}

Three.jsプロジェクトでは、stats-glがこの複雑さを処理します—ほとんどのプロファイリングニーズには生のタイムスタンプクエリの代わりにstats-glを使用してください。

98. コンテキスト喪失を適切に処理

WebGLコンテキストはモバイルで失われる可能性があります。リッスンして回復:

renderer.domElement.addEventListener('webglcontextlost', (event) => {
  event.preventDefault();
  // アニメーションループを停止
});

renderer.domElement.addEventListener('webglcontextrestored', () => {
  // 再初期化
});

99. アニメーションループをプロファイル

各フレームで何が起こっているかを測定:

function animate() {
  const t0 = performance.now();

  physics.update();
  const t1 = performance.now();

  controls.update();
  const t2 = performance.now();

  renderer.render(scene, camera);
  const t3 = performance.now();

  console.log(`Physics: ${t1-t0}ms, Controls: ${t2-t1}ms, Render: ${t3-t2}ms`);

  requestAnimationFrame(animate);
}

100. setAnimationLoopでクリーンなレンダーループ

手動のrequestAnimationFrameの代わりに、Three.js組み込みのアニメーションループを使用:

// 代わりに:
function animate() {
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}
animate();

// 使用:
renderer.setAnimationLoop(() => {
  renderer.render(scene, camera);
});

// 必要なときに停止
renderer.setAnimationLoop(null);

XRセッションを自動処理し、よりクリーンな開始/停止制御を提供します。WebXRアプリケーションには必須です。


Utsuboについて

Utsuboは、ブランドウェブサイトから物理インスタレーションまで、Three.js開発を専門とするインタラクティブクリエイティブスタジオです。

私たちは2024年初頭に2024.utsubo.comで最初期の本番WebGPU Three.js体験をリリースしました。CTOのRenaud RohlingerはThree.js WebGPUレンダラーのコアコントリビューターであり、WebGPUパフォーマンスモニタリング用のstats-glと、高度な3D技術をコンポーザブルなビルディングブロックに凝縮したWebGPUファーストツールキットThree.js Blocksを作成しました。

私たちの実績:

私たちはブランド、美術館、テック企業と共に次世代のウェブ体験を構築しています。


一緒に創りましょう

次の3Dウェブ体験を作るチームをお探しですか?無料ディスカバリーコールをご予約ください。

無料ディスカバリーコールを予約


関連記事


まとめ

以上の100のヒントは、2026年のThree.js本番開発における必須プラクティスをカバーしています:WebGPUレンダラーの導入、DracoとKTX2によるアセット最適化、インスタンシングとバッチングによるドローコール削減、適切なメモリ管理、効果的なデバッグワークフロー。以下では、最適化時によく寄せられる質問にお答えします。


よくある質問

Three.jsのパフォーマンスを最適化するには?

まず測定から始めてください:stats-glとrenderer.infoを使用してボトルネックを特定します。最も一般的な問題は、ドローコールが多すぎる(インスタンシングとバッチングで解決)、最適化されていないアセット(DracoとKTX2圧縮を使用)、メモリリーク(未使用リソースを常にdispose)です。スムーズな60fpsには100未満のドローコールを目標にしてください。

Three.jsでのWebGPUのベストプラクティスは?

r171以降、import { WebGPURenderer } from 'three/webgpu'でゼロコンフィグセットアップと自動WebGL 2フォールバックが利用可能です。クロスプラットフォームシェーダーにはTSL(Three Shader Language)を習得してください。パーティクルシステムや物理にはコンピュートシェーダーを使用。WebGPUはドローコールが多いシーンや計算集約型エフェクトで威力を発揮し、これらのシナリオで2-10倍の改善を実現します。

Three.jsでドローコールを減らすには?

繰り返しオブジェクト(木、パーティクル、小道具)にはInstancedMeshを使用。同じマテリアルだが異なるジオメトリのオブジェクトにはBatchedMeshを使用。メッシュ間でマテリアルを共有。BufferGeometryUtilsで静的ジオメトリをマージ。マテリアルのバリエーションを減らすためにテクスチャアトラスを使用。renderer.info.render.callsで進捗を確認してください。

Three.jsアプリケーションのデバッグに役立つツールは?

必須ツール:FPS/CPU/GPUモニタリングにはstats-gl、パラメータのライブ調整にはlil-gui、WebGLフレームキャプチャにはSpector.js、高速レイキャスティングにはthree-mesh-bvh、メモリとドローコール統計にはrenderer.info、フレームタイミング分析にはブラウザDevToolsのPerformanceタブ。

WebGLからWebGPUに移行すべき?

パフォーマンス限界に達している場合は移行してください—特にドローコールが多いシーン、複雑なパーティクルシステム、計算集約型エフェクトで。新規プロジェクトではWebGPUから始めてください。現在のWebGLプロジェクトがスムーズに動作していてパフォーマンスに制限がない場合、急いで移行する必要はありません。Three.jsは自動フォールバックを提供するので、互換性を壊すことなくWebGPUを導入できます。

Three.jsでのメモリリークを防ぐには?

使用が終わったリソースは常にdispose:geometry.dispose()、material.dispose()、texture.dispose()を呼び出してください。ImageBitmapとして読み込まれたGLTFテクスチャの場合は、texture.source.data.close?.()も呼び出してください。renderer.info.memoryを監視—ジオメトリとテクスチャが増え続ける場合、リークがあります。頻繁に作成・破棄されるオブジェクトにはリソースプーリングを実装してください。

大阪・心斎橋発。記憶に残るWeb体験を。大阪・心斎橋発。記憶に残るWeb体験を。

ストーリー×先端技術で惹きつけ、成果につながる導線まで一貫して設計します。

詳しく見る