Skip to main content

Performance

Optimization techniques for creating smooth, efficient Three.js applications.

Rendering Optimization

Frustum Culling

// Automatic frustum culling is enabled by default
mesh.frustumCulled = true; // Default

// Manual frustum culling
const frustum = new THREE.Frustum();
const matrix = new THREE.Matrix4();

function animate() {
// Update frustum
matrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
frustum.setFromProjectionMatrix(matrix);

// Check if object is visible
scene.traverse(child => {
if (child.isMesh) {
child.visible = frustum.intersectsObject(child);
}
});

renderer.render(scene, camera);
}

Level of Detail (LOD)

const lod = new THREE.LOD();

// Create different detail levels
const highDetailGeometry = new THREE.SphereGeometry(1, 32, 32);
const mediumDetailGeometry = new THREE.SphereGeometry(1, 16, 16);
const lowDetailGeometry = new THREE.SphereGeometry(1, 8, 8);

const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });

const highDetailMesh = new THREE.Mesh(highDetailGeometry, material);
const mediumDetailMesh = new THREE.Mesh(mediumDetailGeometry, material);
const lowDetailMesh = new THREE.Mesh(lowDetailGeometry, material);

// Add levels (mesh, distance)
lod.addLevel(highDetailMesh, 0);
lod.addLevel(mediumDetailMesh, 50);
lod.addLevel(lowDetailMesh, 100);

scene.add(lod);

// Update LOD in animation loop
function animate() {
lod.update(camera);
renderer.render(scene, camera);
requestAnimationFrame(animate);
}

Occlusion Culling

// Simple occlusion culling using raycasting
const raycaster = new THREE.Raycaster();

function isOccluded(object, camera, occluders) {
const direction = new THREE.Vector3()
.subVectors(object.position, camera.position)
.normalize();

raycaster.set(camera.position, direction);

const intersects = raycaster.intersectObjects(occluders);

return (
intersects.length > 0 &&
intersects[0].distance < camera.position.distanceTo(object.position)
);
}

// Use in animation loop
function animate() {
scene.traverse(child => {
if (child.isMesh && child !== occluder) {
child.visible = !isOccluded(child, camera, [occluder]);
}
});

renderer.render(scene, camera);
}

Geometry Optimization

Instanced Rendering

// For rendering many identical objects
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });

const count = 1000;
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);

// Set matrix for each instance
const matrix = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
matrix.setPosition(
Math.random() * 100 - 50,
Math.random() * 100 - 50,
Math.random() * 100 - 50
);
instancedMesh.setMatrixAt(i, matrix);
}

scene.add(instancedMesh);

Merged Geometry

import { BufferGeometryUtils } from 'three/examples/jsm/utils/BufferGeometryUtils.js';

// Merge multiple geometries into one
const geometries = [];

for (let i = 0; i < 100; i++) {
const geometry = new THREE.BoxGeometry(1, 1, 1);
geometry.translate(
Math.random() * 100 - 50,
Math.random() * 100 - 50,
Math.random() * 100 - 50
);
geometries.push(geometry);
}

const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries);
const mesh = new THREE.Mesh(mergedGeometry, material);
scene.add(mesh);

Simplified Geometry

// Use lower-poly versions for distant objects
const simpleGeometry = new THREE.BoxGeometry(1, 1, 1, 1, 1, 1); // 1 segment
const complexGeometry = new THREE.BoxGeometry(1, 1, 1, 10, 10, 10); // 10 segments

// Choose based on distance
function getGeometry(distance) {
return distance > 50 ? simpleGeometry : complexGeometry;
}

Material Optimization

Shared Materials

// Reuse materials instead of creating new ones
const sharedMaterial = new THREE.MeshStandardMaterial({ color: 0x00ff00 });

// Use for multiple meshes
const mesh1 = new THREE.Mesh(geometry1, sharedMaterial);
const mesh2 = new THREE.Mesh(geometry2, sharedMaterial);
const mesh3 = new THREE.Mesh(geometry3, sharedMaterial);

Texture Optimization

// Use appropriate texture sizes
const lowResTexture = textureLoader.load('texture-256.jpg');
const highResTexture = textureLoader.load('texture-2048.jpg');

// Use texture atlases
const atlasTexture = textureLoader.load('texture-atlas.png');

// Optimize texture settings
texture.generateMipmaps = false;
texture.magFilter = THREE.LinearFilter;
texture.minFilter = THREE.LinearFilter;

// Use compressed textures
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath('path/to/basis/');
ktx2Loader.detectSupport(renderer);

Material Switching

// Switch materials based on distance
const highQualityMaterial = new THREE.MeshStandardMaterial({
map: highResTexture,
normalMap: normalTexture,
roughnessMap: roughnessTexture,
});

const lowQualityMaterial = new THREE.MeshBasicMaterial({
map: lowResTexture,
});

function updateMaterials() {
scene.traverse(child => {
if (child.isMesh) {
const distance = camera.position.distanceTo(child.position);
child.material =
distance > 100 ? lowQualityMaterial : highQualityMaterial;
}
});
}

Lighting Optimization

Light Limits

// Limit number of lights affecting each object
const maxLights = 4;

// Use distance property for point lights
const pointLight = new THREE.PointLight(0xffffff, 1, 50); // distance = 50

// Use hemisphere light instead of multiple directional lights
const hemisphereLight = new THREE.HemisphereLight(0x87ceeb, 0x8b4513, 0.5);

Shadow Optimization

// Lower shadow map resolution for less important lights
directionalLight.shadow.mapSize.width = 512;
directionalLight.shadow.mapSize.height = 512;

// Use PCF shadow maps instead of PCF soft shadows
renderer.shadowMap.type = THREE.PCFShadowMap;

// Disable shadows for distant objects
function updateShadows() {
scene.traverse(child => {
if (child.isMesh) {
const distance = camera.position.distanceTo(child.position);
child.castShadow = distance < 100;
child.receiveShadow = distance < 100;
}
});
}

Animation Optimization

Selective Updates

// Only update visible objects
function animate() {
scene.traverse(child => {
if (child.isMesh && child.visible) {
child.rotation.y += 0.01;
}
});

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

Reduced Update Frequency

let frameCount = 0;

function animate() {
frameCount++;

// Update expensive operations less frequently
if (frameCount % 10 === 0) {
updatePhysics();
updateAI();
}

// Update animations every frame
updateAnimations();

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

Memory Management

Object Disposal

// Dispose geometries and materials
function disposeObject(object) {
if (object.geometry) {
object.geometry.dispose();
}

if (object.material) {
if (Array.isArray(object.material)) {
object.material.forEach(material => material.dispose());
} else {
object.material.dispose();
}
}

// Dispose textures
if (object.material && object.material.map) {
object.material.map.dispose();
}
}

// Remove from scene and dispose
scene.remove(mesh);
disposeObject(mesh);

Texture Pool

class TexturePool {
constructor() {
this.pool = new Map();
}

getTexture(url) {
if (!this.pool.has(url)) {
const texture = textureLoader.load(url);
this.pool.set(url, texture);
}
return this.pool.get(url);
}

disposeAll() {
this.pool.forEach(texture => texture.dispose());
this.pool.clear();
}
}

const texturePool = new TexturePool();

Profiling and Monitoring

Performance Monitoring

// Stats.js for FPS monitoring
import Stats from 'three/examples/jsm/libs/stats.module.js';

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

function animate() {
stats.begin();

// Your rendering code
renderer.render(scene, camera);

stats.end();
requestAnimationFrame(animate);
}

Memory Usage

// Monitor memory usage
function logMemoryUsage() {
const info = renderer.info;

console.log('Geometries:', info.memory.geometries);
console.log('Textures:', info.memory.textures);
console.log('Programs:', info.programs.length);
console.log('Calls:', info.render.calls);
console.log('Triangles:', info.render.triangles);
console.log('Points:', info.render.points);
console.log('Lines:', info.render.lines);
}

// Call periodically
setInterval(logMemoryUsage, 5000);

GPU Timing

// Use WebGL timer queries (if supported)
const timerQuery = renderer
.getContext()
.getExtension('EXT_disjoint_timer_query_webgl2');

if (timerQuery) {
const query = renderer.getContext().createQuery();

renderer.getContext().beginQuery(timerQuery.TIME_ELAPSED_EXT, query);
renderer.render(scene, camera);
renderer.getContext().endQuery(timerQuery.TIME_ELAPSED_EXT);

// Check result later
setTimeout(() => {
const result = renderer
.getContext()
.getQueryParameter(query, renderer.getContext().QUERY_RESULT);
console.log('GPU time:', result / 1000000, 'ms');
}, 100);
}

Best Practices

Scene Organization

// Group related objects
const buildings = new THREE.Group();
const vehicles = new THREE.Group();
const characters = new THREE.Group();

scene.add(buildings);
scene.add(vehicles);
scene.add(characters);

// Enable/disable entire groups
buildings.visible = showBuildings;
vehicles.visible = showVehicles;

Render Order

// Control render order for transparency
transparentMesh.renderOrder = 100;
opaqueMesh.renderOrder = 0;

// Use material.transparent carefully
material.transparent = true;
material.opacity = 0.5;

WebGL Context

// Use WebGL 2 if available
const canvas = document.createElement('canvas');
const context = canvas.getContext('webgl2') || canvas.getContext('webgl');

const renderer = new THREE.WebGLRenderer({
canvas: canvas,
context: context,
antialias: true,
powerPreference: 'high-performance',
});

Conditional Rendering

// Skip rendering when not visible
let needsRender = true;

function animate() {
if (needsRender) {
renderer.render(scene, camera);
needsRender = false;
}

requestAnimationFrame(animate);
}

// Trigger render when needed
controls.addEventListener('change', () => {
needsRender = true;
});

Renderer Settings

// Optimize renderer settings
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.physicallyCorrectLights = true;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;

// Disable unnecessary features
renderer.shadowMap.enabled = false; // If shadows not needed
renderer.autoClear = false; // If using multiple render passes