Довідник

Патерни проєктування Three.js

Перевірені патерни та ідіоми для продакшн-застосунків Three.js: структура сцени, керування памʼяттю, адаптивна зміна розміру, пулінг обʼєктів та організація циклу анімації.

Базове налаштування

import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js';

// Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // cap at 2x
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

// Scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0a);

// Camera
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 10);

// Lights
const ambient = new THREE.AmbientLight(0xffffff, 0.5);
const sun = new THREE.DirectionalLight(0xffffff, 2);
sun.position.set(5, 10, 5);
scene.add(ambient, sun);

Адаптивна зміна розміру

Використовуйте ResizeObserver на контейнері канвасу для точного відстеження розміру; уникайте window.onresize, який на мобільних спрацьовує під час прокручування.

// ResizeObserver approach — fires only on actual size change
const ro = new ResizeObserver(entries => {
  const { width, height } = entries[0].contentRect;
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
  renderer.setSize(width, height, false); // false = don't override canvas CSS size
});
ro.observe(renderer.domElement.parentElement ?? document.body);

// Alternative: resize on canvas element itself
const ro2 = new ResizeObserver(() => {
  const w = renderer.domElement.clientWidth;
  const h = renderer.domElement.clientHeight;
  if (renderer.domElement.width !== w || renderer.domElement.height !== h) {
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }
});
ro2.observe(renderer.domElement);

Звільнення памʼяті

Обʼєкти Three.js виділяють памʼять GPU. Завжди звільняйте їх під час видалення обʼєктів зі сцени, щоб запобігти витокам памʼяті.

// Dispose a single mesh completely
function disposeMesh(mesh) {
  mesh.geometry.dispose();

  // Material can be shared — only dispose if not reused elsewhere
  if (Array.isArray(mesh.material)) {
    mesh.material.forEach(m => disposeMaterial(m));
  } else {
    disposeMaterial(mesh.material);
  }

  mesh.removeFromParent();
}

function disposeMaterial(mat) {
  // Dispose all texture maps
  for (const key of Object.keys(mat)) {
    const val = mat[key];
    if (val?.isTexture) val.dispose();
  }
  mat.dispose();
}

// Dispose entire scene recursively
function disposeScene(scene) {
  scene.traverse(obj => {
    if (obj.isMesh) disposeMesh(obj);
  });
}

// Dispose the renderer itself (call when unmounting)
renderer.dispose();
renderer.forceContextLoss();
Викликайте renderer.info.memory до та після звільнення, щоб переконатися, що обʼєкти звільняються. Лічильники geometries і textures мають зменшуватися.

Пулінг обʼєктів

Створення та знищення Vector3, Matrix4 і Quaternion усередині гарячих циклів запускає збирання сміття. Натомість повторно використовуйте їх.

// Pre-allocate temporaries outside loops
const _v  = new THREE.Vector3();
const _q  = new THREE.Quaternion();
const _m  = new THREE.Matrix4();
const _box = new THREE.Box3();

function updateObjects(objects) {
  for (const obj of objects) {
    // Use _ prefixed temporaries — no allocation inside loop
    _v.set(obj.x, obj.y, obj.z);
    _box.setFromCenterAndSize(_v, _v.setScalar(obj.size));
    obj.mesh.position.copy(_v.set(obj.x, obj.y, obj.z));
  }
}

// Pool pattern for mesh instances
class MeshPool {
  constructor(geo, mat, maxSize) {
    this.im = new THREE.InstancedMesh(geo, mat, maxSize);
    this.im.count = 0;
    this._free = Array.from({length: maxSize}, (_, i) => i).reverse();
    this._active = new Set();
  }
  acquire() {
    const id = this._free.pop();
    this._active.add(id);
    this.im.count = Math.max(this.im.count, id + 1);
    return id;
  }
  release(id) {
    this._active.delete(id);
    this._free.push(id);
    // Hide the instance by sending it off-screen
    _m.makeTranslation(99999, 0, 0);
    this.im.setMatrixAt(id, _m);
    this.im.instanceMatrix.needsUpdate = true;
  }
}

Цикл анімації

let lastTime = 0;

function tick(now) {
  const dt = Math.min((now - lastTime) / 1000, 0.1); // seconds, capped at 100ms
  lastTime = now;

  // 1. Update systems (physics, particles, animations)
  updatePhysics(dt);
  updateParticles(dt);
  controls.update();   // OrbitControls damping

  // 2. Render
  renderer.render(scene, camera);

  // 3. Stats (dev mode only)
  // stats.update();
}

// Three.js animation loop (handles XR too)
renderer.setAnimationLoop(tick);

// To stop:
// renderer.setAnimationLoop(null);
Віддавайте перевагу renderer.setAnimationLoop() перед requestAnimationFrame() — він сумісний з WebXR і автоматично викликає колбек із частотою оновлення дисплея.

Ідіоми продуктивності

Позначайте буфери «брудними» лише при зміні
Three.js не знає, коли ви змінюєте масиви атрибутів. Повідомте йому явно.
geo.attributes.position.needsUpdate = true;
geo.attributes.color.needsUpdate = true;
geo.computeBoundingSphere();   // needed for correct frustum culling
Спільне використання геометрій і матеріалів
Кілька мешів можуть спільно використовувати один екземпляр BufferGeometry і Material — Three.js опрацьовує інстансовані юніформи. Спільне використання зменшує перемикання стану GPU.
const geo = new THREE.BoxGeometry();
const mat = new THREE.MeshStandardMaterial({ color: 0xff0000 });
for (let i = 0; i < 1000; i++) {
  scene.add(new THREE.Mesh(geo, mat)); // shared geo + mat
}
Прапорець frustumCulled для систем частинок
Великі обʼєкти частинок Points можуть бути помилково відсічені, якщо обмежувальна сфера застаріла. Встановіть mesh.frustumCulled = false або завжди викликайте computeBoundingSphere() після оновлень.
points.frustumCulled = false; // disable culling entirely for dynamic particles
Текстури зі степенем двійки та генерація mipmap
Текстури не зі степенем двійки не можуть використовувати mipmap у WebGL 1. У WebGL 2 вони працюють, але внутрішньо все одно виділяють памʼять зі степенем двійки. Для текстур даних встановлюйте generateMipmaps = false.
const dataTexture = new THREE.DataTexture(buffer, 200, 100);
dataTexture.type = THREE.FloatType;
dataTexture.format = THREE.RGBAFormat;
dataTexture.minFilter = THREE.LinearFilter;
dataTexture.generateMipmaps = false; // not needed for data / simulation textures
dataTexture.needsUpdate = true;