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