To monitor CPU usage while running your Three.js application, you can utilize Chrome DevTools :
You can easily check the number of draw calls made by the renderer using the following code:
// Monitor draw calls
console.log(renderer.info.render.calls)
For a more detailed performance analysis, you can integrate stats.js into your application. This library provides real-time performance statistics, including frame rates and rendering times. Here’s how to set it up:
import Stats from 'three/examples/jsm/libs/stats.module';
// Create a new Stats instance
const stats = new Stats();
document.body.appendChild(stats.dom);
// Animation loop to update stats
function animate() {
stats.begin(); // Start measuring
// Render your scene here
stats.end(); // Stop measuring
requestAnimationFrame(animate); // Continue the animation loop
}
// Start the animation
animate();
Understanding the performance impact of lights and shadows is crucial for optimizing Three.js applications.
From best to worst performance:
-
No Shadows (Fastest)
- Use for non-essential objects
- Best for mobile/low-end devices
-
DirectionalLight with shadows
- One additional scene render
- Good for sun/moon effects
- Best choice for general shadow casting
-
SpotLight with shadows
- One additional scene render
- Good for focused areas like flashlights
- More expensive than DirectionalLight
-
PointLight with shadows (Most Expensive)
- Six additional scene renders!
- Extremely costly for performance
- Use very sparingly, if at all
Each shadow-casting light adds significant draw calls:
// DirectionalLight/SpotLight
drawCalls = numberOfMeshes * 1
// PointLight (6x more expensive!)
drawCalls = numberOfMeshes * 6
// Example with 5 meshes and 2 point lights:
totalDrawCalls = baseCalls + (meshes * 6 * pointLights)
// 5 base + (5 * 6 * 2) = 65 draw calls
Good draw calls for web-games : <200
// GOOD - Main shadow casting light
const mainLight = new THREE.DirectionalLight()
mainLight.castShadow = true
mainLight.position.set(50, 30, 50)
mainLight.intensity = 1.5
// GOOD - Ambient fill light for overall scene brightness
const ambientLight = new THREE.AmbientLight('#ffffff', 0.4)
// GOOD - Hemisphere light for sky/ground color variation
const hemiLight = new THREE.HemisphereLight(
'#skyColor', // Sky color
'#groundColor', // Ground color
0.5 // Intensity
)
// ACCEPTABLE - Non-shadow point light for local highlights
const accentLight = new THREE.PointLight('#ffffff', 1.0)
accentLight.castShadow = false // Important!
// BAD - Point light with shadows (extremely expensive!)
const badLight = new THREE.PointLight('#ffffff', 1.0)
badLight.castShadow = true // Will cause 6 additional renders!
From best to worst performance:
-
AmbientLight
- No shadows possible
- Extremely cheap
- Good for base illumination
- Use to reduce shadow darkness
-
HemisphereLight
- No shadows possible
- Very performant
- Great for outdoor scenes
- Provides subtle color variation
-
DirectionalLight (without shadows)
- Good for sun/moon simulation
- Relatively cheap
- Consistent shadows across scene
-
DirectionalLight (with shadows)
- One additional render pass
- Best choice for main shadow caster
- Good balance of quality/performance
-
SpotLight (without shadows)
- Good for focused lighting
- Moderate performance impact
- Use for highlights/accents
-
SpotLight (with shadows)
- One additional render pass
- More expensive than DirectionalLight
- Use sparingly
-
PointLight (without shadows)
- Expensive but manageable
- Good for local lighting
- Keep radius small when possible
-
PointLight (with shadows)
⚠️ - EXTREMELY expensive (6 render passes!)
- Should almost never be used
- Consider alternatives:
- SpotLight with shadows
- Multiple non-shadow PointLights
- Baked shadows/lighting
// Basic Outdoor Scene
const lights = {
main: new THREE.DirectionalLight('#ffffff', 1.5), // Sun
hemi: new THREE.HemisphereLight('#skyblue', '#groundcolor', 0.5),
ambient: new THREE.AmbientLight('#ffffff', 0.2)
}
lights.main.castShadow = true
// Indoor Scene
const lights = {
main: new THREE.DirectionalLight('#ffffff', 0.5), // Window light
spots: [
new THREE.SpotLight('#ffffff', 0.7), // Key light
new THREE.SpotLight('#ffffff', 0.3) // Fill light
],
ambient: new THREE.AmbientLight('#ffffff', 0.3)
}
lights.main.castShadow = true
// Not needed since it's by default at false, but just to show usage
lights.spots.forEach(light => light.castShadow = false)
// Night Scene
const lights = {
moon: new THREE.DirectionalLight('#blue', 0.5),
points: Array(5).fill(0).map(() => new THREE.PointLight('#orange', 0.4)),
ambient: new THREE.AmbientLight('#000033', 0.2)
}
lights.moon.castShadow = true
// Not needed since it's by default at false, but just to show usage
lights.points.forEach(light => light.castShadow = false) // Important!
- Use ONE main shadow-casting light (usually DirectionalLight)
- Combine AmbientLight and HemisphereLight for base illumination
- Use non-shadow PointLights for accent lighting
- Avoid shadow-casting SpotLights when possible
- NEVER use shadow-casting PointLights in production
- Consider light radius/distance for PointLights and SpotLights
- Use light helpers during development to visualize coverage
const directionalLight = new THREE.DirectionalLight()
// Reduce shadow map size for better performance
// Set the resolution of the shadow map texture (higher = sharper shadows but more expensive)
directionalLight.shadow.mapSize.height = 1024
directionalLight.shadow.mapSize.width = 1024
// Only enable shadows on important objects
mainObject.traverse((child) => {
if (child.isMesh) {
child.castShadow = false // Disable casting
child.receiveShadow = true // Still receive shadows
}
})
- Use ONE DirectionalLight for main shadows
- Add non-shadow PointLights for ambient lighting
- Avoid shadow-casting PointLights
- Consider baking shadows for static scenes
- Use smaller shadow maps (512x512) by default
- Increase only for close-up shadows
- Disable
autoUpdate
for static scenes - Consider shadow bias adjustment for quality/performance balance
- Combine small meshes where possible
- Use instancing for repeated objects
- Target < 200 draw calls for web
- Group static objects
// Shadows require appropriate materials
const material = new THREE.MeshStandardMaterial({
receiveShadow: true,
castShadow: true
})
// BasicMaterial doesn't support shadows
const basicMaterial = new THREE.MeshBasicMaterial() // No shadows
-
autoUpdate
: Enables automatic updates to the shadows in the scene- Default is
true
- Default is
-
When
autoUpdate
isfalse
, you need to manually update the shadows maps withneedsUpdate
-
needsUpdate
: Forces shadow maps to update on next render- Default is
false
- Required when
autoUpdate = false
to manually trigger shadow updates - Must be set to
true
followed by a render call to update shadows
- Default is
renderer.shadowMap.autoUpdate = false
// Update shadows only when necessary
function onObjectMove() {
renderer.shadowMap.needsUpdate = true
}
renderer.shadowMap.autoUpdate = false
// Update shadows only once per second
function updateShadows() {
renderer.shadowMap.needsUpdate = true
}
setInterval(updateShadows, 1000)
// Of course, you can use requestAnimationFrame instead of setInterval with proper delta time logic
-
Too Many Shadow Casters
- Every shadow-casting light multiplies render calls
- Each mesh with
castShadow = true
adds to the cost
-
Unnecessary Shadow Updates
- Static scenes don't need
autoUpdate
- Update shadows only when the scene changes
- Static scenes don't need
-
Oversized Shadow Maps
- Large shadow maps impact memory and performance
- Start small and increase only if needed
-
Point Light Shadows
- Extremely expensive (6x normal shadow cost)
- Consider alternatives like SpotLights
Always test on target devices:
- Mobile performance differs significantly
- Monitor FPS and draw calls
- Use Chrome DevTools Performance tab
- Test with varying numbers of objects and lights
THREE-CSM is a solution for rendering high-quality directional light shadows over large distances. It works by splitting the view frustum into several segments, each with its own shadow map.
- Better shadow quality over large distances
- More efficient than increasing shadow map resolution
- Good for open world scenes
import { CSM } from 'three-csm'
// Create CSM instance
const csm = new CSM({
maxFar: 1000, // How far shadows are rendered
cascades: 4, // Number of shadow cascades
shadowMapSize: 1024, // Resolution of each cascade
lightDirection: new THREE.Vector3(-1, -1, -1),
camera: camera, // Your THREE.Camera instance
parent: scene // Your THREE.Scene instance
})
// Update in animation loop
function animate() {
csm.update(camera.matrix)
renderer.render(scene, camera)
}
- Each cascade adds one render pass
- Balance cascade count with performance needs
- Typically 3-4 cascades is sufficient
- Adjust
maxFar
andshadowMapSize
based on scene scale
- Large outdoor environments
- When shadow quality at distance is important
- Games with terrain or large landscapes
- When standard shadows show obvious cutoff points
// High-end desktop setup
const csmHigh = new CSM({
cascades: 4,
shadowMapSize: 2048,
maxFar: 2000,
mode: 'practical'
})
// Mobile-friendly setup
const csmLow = new CSM({
cascades: 2,
shadowMapSize: 512,
maxFar: 500,
mode: 'uniform'
})
Note: THREE-CSM might need updates for newer Three.js versions. Check compatibility before implementation.
A previous project demonstrating good performance practices:
- Live demo: https://enari-engine.vercel.app/
- Source code: https://github.com/iErcann/enari-engine
Also, the default scene uses baked shadows (found it on Sketchfab) You can make your own worlds with static baked shadows (Blender)
https://github.com/iErcann/Notblox/blob/main/PERFORMANCE.md