Offload Three.js Geometry Generation to Web Workers
Generating complex 3D geometries in Three.js can cause noticeable
frame rate drops and freeze the user interface because JavaScript runs
on a single thread. To maintain a smooth 60 FPS, you can offload these
CPU-intensive mathematical calculations to a Web Worker. This article
demonstrates how to compute geometry vertex data in a background thread
and transfer it back to the main thread to construct a
BufferGeometry without blocking the UI.
The Challenge with Web Workers and Three.js
Web Workers run in an isolated environment that lacks access to the
DOM, the window object, and the WebGL context. Consequently, you cannot
instantiate Three.js objects like THREE.BufferGeometry or
THREE.Mesh directly inside a worker.
To overcome this, you must generate the raw data (like vertices,
normals, and UVs) as typed arrays (such as Float32Array) in
the Web Worker. You then transfer these arrays back to the main thread
using Transferable Objects, which transfer memory
ownership instantly without copying the data, ensuring maximum
performance.
Step 1: Create the Web Worker
In your worker script (e.g., geometry.worker.js),
perform the heavy mathematical calculations to generate the vertex
positions. Once completed, post the raw buffer back to the main
thread.
// geometry.worker.js
self.onmessage = function (e) {
const { segments } = e.data;
// Example: Generate a custom terrain grid
const vertexCount = (segments + 1) * (segments + 1);
const positions = new Float32Array(vertexCount * 3);
let index = 0;
for (let x = 0; x <= segments; x++) {
for (let z = 0; z <= segments; z++) {
const posX = x - segments / 2;
const posZ = z - segments / 2;
// Simulate heavy mathematical calculation (e.g., Perlin noise)
const posY = Math.sin(posX * 0.1) * Math.cos(posZ * 0.1) * 2;
positions[index++] = posX;
positions[index++] = posY;
positions[index++] = posZ;
}
}
// Pass the Float32Array back to the main thread as a Transferable Object
self.postMessage({ positions }, [positions.buffer]);
};Step 2: Handle the Worker on the Main Thread
On the main thread, instantiate the worker, send the generation
parameters, and listen for the computed array. Once the data is
received, assign it to a BufferGeometry and add it to your
Three.js scene.
import * as THREE from 'three';
// 1. Initialize the Web Worker
const worker = new Worker(new URL('./geometry.worker.js', import.meta.url));
// 2. Request geometry generation
worker.postMessage({ segments: 200 });
// 3. Receive the generated data from the worker
worker.onmessage = function (e) {
const { positions } = e.data;
// 4. Create a new BufferGeometry
const geometry = new THREE.BufferGeometry();
// 5. Apply the transferred Float32Array to the geometry position attribute
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
// 6. Compute normals for proper lighting
geometry.computeVertexNormals();
// 7. Create the mesh and add it to the scene
const material = new THREE.MeshStandardMaterial({ color: 0x3b82f6, wireframe: true });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
console.log('Geometry successfully generated in background thread and rendered.');
};Key Performance Best Practices
- Use Transferable Objects: Always pass the
underlying
ArrayBufferin the second argument ofpostMessage()(e.g.,[positions.buffer]). This avoids a costly serialization and cloning process, rendering the transfer near-instantaneous. - Reuse Arrays: If you are constantly regenerating geometry (such as dynamic terrain deformation), instantiate the typed arrays once and pass them back and forth between the main thread and the worker to avoid garbage collection overhead.
- Batch Operations: Avoid sending hundreds of small messages. Collect all necessary generation parameters into a single message, and return all attributes (positions, normals, UVs, indices) in a single response payload.