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