Handle WebGL Context Loss and Restore in Three.js

Operating systems or graphics drivers can occasionally reset the GPU, causing a WebGL context loss that crashes your Three.js application. This article explains why WebGL context loss occurs in Three.js and provides a step-by-step guide on how to detect this event, pause your application, and successfully restore your 3D scene without requiring a full page reload.

Understanding WebGL Context Loss

WebGL context loss occurs when the GPU resets or becomes unavailable. This can be triggered by hardware mode switches, driver updates, system sleep cycles, or when the browser decides the GPU is overloaded. When the context is lost, all WebGL resources—such as textures, geometries, shaders, and framebuffers—are cleared from the GPU memory.

To handle this gracefully, you must monitor the WebGL canvas for context events, pause your render loop, and rebuild your resources once the context is restored.

Step 1: Detect the Context Loss Event

To detect when a context is lost, add an event listener to the canvas element associated with your Three.js WebGLRenderer. You must call event.preventDefault() inside this listener; otherwise, the browser will not attempt to restore the context.

const canvas = renderer.domElement;

canvas.addEventListener('webglcontextlost', (event) => {
    event.preventDefault();
    console.warn('WebGL Context Lost. Pausing application...');
    
    // Stop the rendering loop to prevent console errors
    stopAnimationLoop();
    
    // Execute custom clean-up logic
    onContextLost();
}, false);

Step 2: Handle the Context Restoration Event

After the GPU recovers, the browser fires a webglcontextrestored event. In this listener, you must reinitialize your renderer state, recreate textures or materials if necessary, and restart your animation loop.

canvas.addEventListener('webglcontextrestored', () => {
    console.log('WebGL Context Restored. Reinitializing...');
    
    // Reset the renderer's internal state
    renderer.forceContextRestore();
    
    // Rebuild lost assets (textures, shaders, render targets)
    rebuildResources();
    
    // Restart the rendering loop
    startAnimationLoop();
}, false);

Step 3: Rebuilding Three.js Resources

While Three.js automatically attempts to re-upload standard geometries and materials to the GPU upon context restoration, custom WebGL assets require manual intervention:

  1. Textures: Re-load or re-assign textures. Call texture.needsUpdate = true on all active textures to force Three.js to re-upload them to the new GPU context.
  2. Custom Shaders: Recompile custom ShaderMaterial instances. Setting material.needsUpdate = true forces Three.js to recompile the shaders.
  3. Render Targets: If your application uses WebGLRenderTarget for post-processing or shadows, dispose of the old targets using .dispose() and instantiate new ones.
function rebuildResources() {
    // Traverse the scene to flag materials and textures for re-upload
    scene.traverse((object) => {
        if (object.isMesh) {
            if (object.material) {
                if (Array.isArray(object.material)) {
                    object.material.forEach(mat => resetMaterial(mat));
                } else {
                    resetMaterial(object.material);
                }
            }
        }
    });
}

function resetMaterial(material) {
    material.needsUpdate = true;
    if (material.map) material.map.needsUpdate = true;
    if (material.lightMap) material.lightMap.needsUpdate = true;
    if (material.bumpMap) material.bumpMap.needsUpdate = true;
    // Repeat for any other texture maps used in your materials
}

Testing Context Loss

You can simulate a WebGL context loss to test your implementation. Use the WebGL extension WEBGL_lose_context to manually trigger the event in your development console:

const extension = renderer.getContext().getExtension('WEBGL_lose_context');

// Simulate context loss
extension.loseContext();

// Simulate context restoration after 3 seconds
setTimeout(() => {
    extension.restoreContext();
}, 3000);