PROWAREtech
ThreeJS: Performance Tips
Do as little processing as possible in the animation loop. This will help to increase the frames per second performance dramatically. For example, instead of checking the scroll position to position objects in the animation loop, do it all during the scroll event.
Reuse mesh materials whenever possible. In the following example, the choice is given to reuse materials or to create materials for every new 3D object. Notice that reusing the mesh material results in a higher frames-per-second performance.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Performance</title>
<style>
canvas {
position: fixed;
top: 0;
left: 0;
display: block;
width: 100%;
height: 100vh;
}
#fps {
position: fixed;
top: 0;
left: 0;
background-color: rgba(0,0,0,0.5);
color: white;
padding: 5px;
}
</style>
</head>
<body>
<canvas></canvas>
<div id="fps">0 FPS</div>
<script src="/js/three.min.js"></script>
<script type="text/javascript">
(function () {
var createCubeGridReuse = function (gridSize, cubeSize, gap) {
var x = 0, y = 0, z = 0, rows, cols, colors = [
new THREE.MeshPhongMaterial({ color: 0x666666 }),
new THREE.MeshPhongMaterial({ color: 0x999999 }),
new THREE.MeshPhongMaterial({ color: 0xCCCCCC })
], group = new THREE.Group();
gap = Math.abs(gap);
gridSize = Math.pow(gridSize, 1 / 3);
if (gridSize % 1 != 0) {
gridSize = Math.round(gridSize);
}
rows = cols = gridSize;
for (var row = 0; row < rows; row++) {
for (var col = 0; col < cols; col++) {
for (var depth = 0; depth < rows; depth++) {
var position = cubeSize / 2 * rows / 2 + gap;
x = position * row;
y = position * col;
z = position * depth;
var num = (row + col + depth) % 3;
var mesh = new THREE.Mesh(new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize), colors[num]);
mesh.receiveShadow = mesh.castShadow = true;
mesh.position.set(x, y, z);
group.add(mesh);
}
}
}
group.position.set(x / -2, y / -2, z / -2);
var grid = new THREE.Group();
grid.add(group);
return grid;
};
var createCubeGrid = function (gridSize, cubeSize, gap) {
var x = 0, y = 0, z = 0, rows, cols, colors = [0x666666, 0x999999, 0xCCCCCC], group = new THREE.Group();
gap = Math.abs(gap);
gridSize = Math.pow(gridSize, 1 / 3);
if (gridSize % 1 != 0) {
gridSize = Math.round(gridSize);
}
rows = cols = gridSize;
console.log(Math.pow(gridSize, 3) + " cubes");
for (var row = 0; row < rows; row++) {
for (var col = 0; col < cols; col++) {
for (var depth = 0; depth < rows; depth++) {
var position = cubeSize / 2 * rows / 2 + gap;
x = position * row;
y = position * col;
z = position * depth;
var num = (row + col + depth) % 3;
var mesh = new THREE.Mesh(new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize), new THREE.MeshPhongMaterial({ color: colors[num] }));
mesh.receiveShadow = mesh.castShadow = true;
mesh.position.set(x, y, z);
group.add(mesh);
}
}
}
group.position.set(x / -2, y / -2, z / -2);
var grid = new THREE.Group();
grid.add(group);
return grid;
};
var canvas = document.getElementsByTagName("canvas")[0];
// NOTE: create the scene to place objects in
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x6699CC);
scene.matrixWorldAutoUpdate = true;
// NOTE: the width and height of the canvas
var size = {
width: canvas.offsetWidth,
height: canvas.offsetHeight
};
var cameraNear = 1, cameraFar = 500;
var camera = new THREE.PerspectiveCamera(75, size.width / size.height, cameraNear, cameraFar);
// NOTE: position the camera in space a bit
camera.position.z = 30;
var renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true
});
renderer.shadowMap.enabled = true; // NOTE: must enable shadows on the renderer
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(size.width, size.height);
renderer.render(scene, camera);
var light = new THREE.DirectionalLight(0xffffff, 1.5);
light.position.set(2, 2, 2);
light.castShadow = true;
light.shadow.mapSize.width = 1024;
light.shadow.mapSize.height = 1024;
light.shadow.camera.near = cameraNear;
light.shadow.camera.far = cameraFar;
scene.add(light);
scene.add(new THREE.AmbientLight(0xffffff, .3));
var reuse = confirm("Okay to be efficient and reuse mesh materials for a greater frames-per-second performance?");
var num = parseInt(prompt("Enter grid size:", 21) || 21) || 21;
var size = num*num*num;
if(!confirm("This will create " + size + " cubes.")) { alert("Aborting!"); return; }
var grid = reuse ? createCubeGridReuse(size, 0.2, 0.001) : createCubeGrid(size, 0.2, 0.001);
scene.add(grid);
var frame = Math.PI / 192;
var frameCount = 0;
var getFrameCount = function () {
document.getElementById("fps").innerText = frameCount + " FPS, reuse: " + (reuse ? "yes" : "no");
frameCount = 0;
};
setInterval(getFrameCount, 1000);
// NOTE: MUST HAVE AN ANIMATE FUNCTION
var animate = function () {
frameCount++;
grid.rotation.x += frame;
grid.rotation.y += frame;
renderer.render(scene, camera);
requestAnimationFrame(animate);
};
animate();
})();
</script>
</body>
</html>
Setting the antialias
property of the renderer to false
will increase the performance but at the cost of image quality, even on high resolution monitors.
Instead of adding and removing lights from a scene set their intensity to low values. This is because the renderer must recompile the shader program. The THREE.DirectionalLight
is a good lightweight light to use.
Set the camera frustum to be as little as possible to support the scene.
Using models, like glTF ones, just slows things down, particularly for complex 3D models. Sometimes they are required but they do not always play nice in the world of THREE.js. Shadows can create problems, for example.
The penalty to using the highest quality mesh THREE.MeshStandardMaterial
is negligible. The performance difference between this and THREE.MeshBasicMaterial
is about 10% (if that), so using a lesser mesh material like THREE.MeshPhongMaterial
is really only required for the largest projects or for adapting to lesser hardware. It is a popular myth that the higher quality materials come with a big performance penalty, but the THREE developers have probably made some performance increases (through caching) that make the penalty less now than when this myth was originally floated.
In order of lowest to highest performance:
THREE.MeshStandardMaterial
THREE.MeshPhongMaterial
THREE.MeshLambertMaterial
THREE.MeshBasicMaterial
Bear in mind that changing properties of the mesh material will affect performance.
This example compares both THREE.MeshStandardMaterial
(the highest image quality) and THREE.MeshBasicMaterial
(the lowest):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Performance</title>
<style>
canvas {
position: fixed;
top: 0;
left: 0;
display: block;
width: 100%;
height: 100vh;
}
#fps {
position: fixed;
top: 0;
left: 0;
background-color: rgba(0,0,0,0.5);
color: white;
padding: 5px;
}
</style>
</head>
<body>
<canvas></canvas>
<div id="fps">0 FPS</div>
<script src="/js/three.min.js"></script>
<script type="text/javascript">
(function () {
var createCubeGridHi = function (gridSize, cubeSize, gap) {
var x = 0, y = 0, z = 0, rows, cols, colors = [
new THREE.MeshStandardMaterial({ color: 0x666666, roughness: 0.5, metalness: 0.5 }),
new THREE.MeshStandardMaterial({ color: 0x999999, roughness: 0.5, metalness: 0.5 }),
new THREE.MeshStandardMaterial({ color: 0xCCCCCC, roughness: 0.5, metalness: 0.5 })
], group = new THREE.Group();
gap = Math.abs(gap);
gridSize = Math.pow(gridSize, 1 / 3);
if (gridSize % 1 != 0) {
gridSize = Math.round(gridSize);
}
rows = cols = gridSize;
for (var row = 0; row < rows; row++) {
for (var col = 0; col < cols; col++) {
for (var depth = 0; depth < rows; depth++) {
var position = cubeSize / 2 * rows / 2 + gap;
x = position * row;
y = position * col;
z = position * depth;
var num = (row + col + depth) % 3;
var mesh = new THREE.Mesh(new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize), colors[num]);
mesh.receiveShadow = mesh.castShadow = true;
mesh.position.set(x, y, z);
group.add(mesh);
}
}
}
group.position.set(x / -2, y / -2, z / -2);
var grid = new THREE.Group();
grid.add(group);
return grid;
};
var createCubeGridLo = function (gridSize, cubeSize, gap) {
var x = 0, y = 0, z = 0, rows, cols, colors = [
new THREE.MeshBasicMaterial({ color: 0x666666 }),
new THREE.MeshBasicMaterial({ color: 0x999999 }),
new THREE.MeshBasicMaterial({ color: 0xCCCCCC })
], group = new THREE.Group();
gap = Math.abs(gap);
gridSize = Math.pow(gridSize, 1 / 3);
if (gridSize % 1 != 0) {
gridSize = Math.round(gridSize);
}
rows = cols = gridSize;
for (var row = 0; row < rows; row++) {
for (var col = 0; col < cols; col++) {
for (var depth = 0; depth < rows; depth++) {
var position = cubeSize / 2 * rows / 2 + gap;
x = position * row;
y = position * col;
z = position * depth;
var num = (row + col + depth) % 3;
var mesh = new THREE.Mesh(new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize), colors[num]);
mesh.receiveShadow = mesh.castShadow = true;
mesh.position.set(x, y, z);
group.add(mesh);
}
}
}
group.position.set(x / -2, y / -2, z / -2);
var grid = new THREE.Group();
grid.add(group);
return grid;
};
var canvas = document.getElementsByTagName("canvas")[0];
// NOTE: create the scene to place objects in
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x6699CC);
scene.matrixWorldAutoUpdate = true;
// NOTE: the width and height of the canvas
var size = {
width: canvas.offsetWidth,
height: canvas.offsetHeight
};
var cameraNear = 1, cameraFar = 500;
var camera = new THREE.PerspectiveCamera(75, size.width / size.height, cameraNear, cameraFar);
// NOTE: position the camera in space a bit
camera.position.z = 70;
var renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true
});
renderer.shadowMap.enabled = true; // NOTE: must enable shadows on the renderer
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(size.width, size.height);
renderer.render(scene, camera);
var light = new THREE.DirectionalLight(0xffffff, 1.5);
light.position.set(2, 2, 2);
light.castShadow = true;
light.shadow.mapSize.width = 1024;
light.shadow.mapSize.height = 1024;
light.shadow.camera.near = cameraNear;
light.shadow.camera.far = cameraFar;
scene.add(light);
scene.add(new THREE.AmbientLight(0xffffff, .3));
var hi = confirm("Okay to use highest quality mesh materials?");
var num = parseInt(prompt("Enter grid size:", 25) || 25) || 25;
var size = num*num*num;
if(!confirm("This will create " + size + " cubes.")) { alert("Aborting!"); return; }
var grid = hi ? createCubeGridHi(size, 0.5, 0.0001) : createCubeGridLo(size, 0.5, 0.0001);
scene.add(grid);
var frame = Math.PI / 192;
var frameCount = 0;
var getFrameCount = function () {
document.getElementById("fps").innerText = frameCount + " FPS, hi: " + (hi ? "yes" : "no");
frameCount = 0;
};
setInterval(getFrameCount, 1000);
// NOTE: MUST HAVE AN ANIMATE FUNCTION
var animate = function () {
frameCount++;
grid.rotation.x += frame;
grid.rotation.y += frame;
renderer.render(scene, camera);
requestAnimationFrame(animate);
};
animate();
})();
</script>
</body>
</html>