How to code performative projects with WebGL & Three.js - 5 3D performance tips

How to code performative projects with WebGL & Three.js - 5 3D performance tips

One of the main things to do while preparing a project with threejs is to target a 60fps experience, at least. Some users might even have configurations where the experience should run at a higher frame rate. There are to main factors that help us know why this happends:

  • The CPU
  • The GPU

You need to keep an eye on the performances and test across multiple devices with different setups and don't forget mobile devices if your website is supposed to be compatible with those.

1. Monitoring draw calls

Draw calls are actions of drawing triangles by the GPU. There will be many draw calls when we have a complex scene with many objects, geometries, materials, etc.

Usually, we can say that the less draw calls you have, the better. We will see some tips to reduce these, but first, we would like to monitor them.

There is a great Chrome extension named Spector.js that can help you with that.

  • Install the extension: spectorjs
  • On the WebGL page, click on the extension icon to activate it.
  • Click again to open the extension panel.
  • Click on the red circle to record the frame.

Wait a little, and a new page will open with many intricate details about the recorded frame.

In the Commands tab, you'll see how the frame has been drawn step by step. We won't explain everything here, but the blue steps are draw calls, and the other steps are usually data sent to the GPU such as the matrices, attributes, uniforms, etc.

The less you have, the better.

2. Dispose unused meshes and models

Once you are absolutely sure you don't need a resource like a geometry or a material, dispose of it. If you create a game with levels, once the user goes to the next level, dispose of things from the previous level.

To do that, there is a dedicated page on the Three.js documentation: threejs.org/docs/#manual/en/introduction/Ho..

Here's an example with a sphere:

scene.remove(sphere)
sphere.geometry.dispose()
sphere.material.dispose()

3. Optimize shadow maps

If you don't have any other choice, try to optimize the shadow maps so they look good but fit perfectly with the scene.

Use the CameraHelper to see the area that will be renderer by the shadow map camera and reduce it to the smallest area possible:

directionalLight.shadow.camera.top = 3
directionalLight.shadow.camera.right = 6
directionalLight.shadow.camera.left = - 6
directionalLight.shadow.camera.bottom = - 3
directionalLight.shadow.camera.far = 10

const cameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
scene.add(cameraHelper)

4. Mutualize geometries

If you have multiple Meshes using the same geometry shape, create only one geometry, and use it on all the meshes:

const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5)

for(let i = 0; i < 50; i++)
{
    const material = new THREE.MeshNormalMaterial()

    const mesh = new THREE.Mesh(geometry, material)
    mesh.position.x = (Math.random() - 0.5) * 10
    mesh.position.y = (Math.random() - 0.5) * 10
    mesh.position.z = (Math.random() - 0.5) * 10
    mesh.rotation.y = (Math.random() - 0.5) * Math.PI * 2
    mesh.rotation.z = (Math.random() - 0.5) * Math.PI * 2

    scene.add(mesh)
}

5. Keep code simple

It's laborious to monitor the difference, but try to keep your shader codes as simple as possible. Avoid if statements. Make good use of swizzles and built-in functions.

As in the vertex shader, instead of the if statement:

modelPosition.y += clamp(elevation, 0.5, 1.0) * uDisplacementStrength;

Or as in the fragment shader, instead of these complex formulas for r, g and b:

vec3 depthColor = vec3(1.0, 0.1, 0.1);
vec3 surfaceColor = vec3(0.1, 0.0, 0.5);
vec3 finalColor = mix(depthColor, surfaceColor, elevation);

Keep an eye on the performances from the start. Test on other devices, use the tools we saw initially and fix any strange behavior before going further.

Each project will have different constraints, and applying those tips won't always suffice. Try to find solutions. Twist the way you are doing things. Be smart.

This article was possible thanks to the Three.js journey course by Bruno Simon