PROWAREtech

articles » current » javascript » three-js » performance-tips

ThreeJS: Performance Tips

How to increase and not to increase the performance of your THREE.js application.

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:

  1. THREE.MeshStandardMaterial
  2. THREE.MeshPhongMaterial
  3. THREE.MeshLambertMaterial
  4. 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>

PROWAREtech

Hello there! How can I help you today?
Ask any question

PROWAREtech

This site uses cookies. Cookies are simple text files stored on the user's computer. They are used for adding features and security to this site. Read the privacy policy.
CLOSE