How to Use Three.js compileAsync Before Render Loop
When rendering 3D scenes in WebGL, shader compilation often causes a
noticeable pause or “stutter” the first time a new material or object
enters the camera’s view. This article demonstrates how to eliminate
this lag by utilizing the asynchronous
WebGLRenderer.compileAsync() method in Three.js, allowing
you to force the compilation of all scene shaders prior to starting your
render loop.
The Problem: Shader Compilation Stutter
By default, Three.js compiles shaders lazily. This means compilation occurs right when an object is about to be drawn to the screen for the first time. Because compiling GLSL shaders is a CPU-intensive task that blocks the main thread, this lazy compilation results in dropped frames (jank).
While the older renderer.compile(scene, camera) method
compiles shaders upfront, it runs synchronously on the main thread,
which can freeze the browser UI.
The Solution: Using compileAsync
The WebGLRenderer.compileAsync() method compiles all
materials present in the scene asynchronously. By pairing this method
with JavaScript’s async/await syntax, you can keep the
browser responsive (e.g., displaying a loading spinner) and delay the
start of your rendering loop until every shader is ready.
Implementation Code Example
Here is how to set up your initialization sequence to block the render loop until all shaders are fully compiled:
import * as THREE from 'three';
// 1. Setup basic scene, camera, and renderer
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 2. Add your meshes, lights, and complex materials to the scene
const geometry = new THREE.TorusKnotGeometry(10, 3, 100, 16);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00, roughness: 0.1 });
const torusKnot = new THREE.Mesh(geometry, material);
scene.add(torusKnot);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(1, 1, 1).normalize();
scene.add(light);
camera.position.z = 30;
// 3. Define the render loop
function animate() {
requestAnimationFrame(animate);
// Rotate object to show smooth performance
torusKnot.rotation.x += 0.01;
torusKnot.rotation.y += 0.01;
renderer.render(scene, camera);
}
// 4. Create an async initialization function
async function prepareAndRender() {
const loadingScreen = document.getElementById('loading-screen');
console.log('Compiling shaders asynchronously...');
try {
// Pre-compile all materials currently in the scene
await renderer.compileAsync(scene, camera);
console.log('All shaders compiled. Starting render loop.');
// Hide loading screen UI
if (loadingScreen) {
loadingScreen.style.display = 'none';
}
// Start the render loop safely
animate();
} catch (error) {
console.error('An error occurred during shader compilation:', error);
}
}
// Start the process
prepareAndRender();How It Works
- Scene Preparation: All meshes, lights, and
materials must be added to the scene before calling
compileAsync. The method works by parsing the existing scene graph and preparing the WebGL programs for everything it finds. - Asynchronous Waiting: The
await renderer.compileAsync(scene, camera)statement pauses the execution of theprepareAndRenderfunction. However, because it is asynchronous, it does not freeze the browser’s main thread, allowing loading animations to continue playing. - Delayed Loop Trigger: The
animate()render loop function is only called after the promise returned bycompileAsyncsuccessfully resolves. This guarantees that your first frame—and all subsequent frames—will render immediately without any compilation overhead.