Quick answer: Listen for webglcontextlost with preventDefault(), then rebuild every texture, buffer, and program on webglcontextrestored. Plan for this from day one; retrofitting is expensive.
A player switches to a different tab for two minutes. Returns to your game. The canvas is black, the engine throws “INVALID_OPERATION” errors, and the game is unrecoverable without a page reload. The WebGL context was lost while in the background and your engine doesn’t know how to restore it.
The WebGL Context Loss Lifecycle
Two events fire on the canvas:
- webglcontextlost — the context is gone. All GL resources are invalid. The default action is to leave the context dead; calling
event.preventDefault()tells the browser to attempt restoration. - webglcontextrestored — the browser has restored the context with a fresh GL state. Rebuild your resources from CPU-side data.
Step 1: Subscribe to Events
canvas.addEventListener("webglcontextlost", (e) => {
e.preventDefault(); // opt into restoration
pauseGame();
contextLost = true;
}, false);
canvas.addEventListener("webglcontextrestored", () => {
rebuildAllResources();
contextLost = false;
resumeGame();
}, false);
Without preventDefault(), the browser refuses to attempt restoration and you must reload the page. With it, the browser may restore in seconds to minutes (or immediately on tab focus).
Step 2: Keep CPU-Side Copies
For every GPU resource, retain enough CPU data to recreate it:
- Textures: keep the
ImageBitmaporHTMLImageElementsource (or a typed array if procedurally generated). - Buffers: keep the
Float32ArrayorUint16Arraysource data. - Shaders: keep the source strings (you should anyway).
- Framebuffers: recreate textures attached to them.
class Texture {
constructor(image) {
this.image = image; // keep CPU reference
this.create();
}
create() {
this.handle = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, this.handle);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.image);
// ... filtering, mipmaps, etc.
}
recreate() { this.create(); }
}
On webglcontextrestored, call recreate() on every Texture in your asset table. Same for buffers, programs.
Step 3: Resource Registry
Maintain a registry of every GPU object so you can iterate and recreate:
const registry = {
textures: new Set(),
buffers: new Set(),
programs: new Set(),
};
function rebuildAllResources() {
for (const t of registry.textures) t.recreate();
for (const b of registry.buffers) b.recreate();
for (const p of registry.programs) p.recreate();
}
Have each resource class register itself in its constructor and unregister in dispose(). Order matters: programs depend on shaders; framebuffers depend on textures. Recreate in dependency order.
Step 4: Test with a Manual Context Loss
const ext = gl.getExtension("WEBGL_lose_context");
// Trigger context loss for testing
ext.loseContext();
setTimeout(() => ext.restoreContext(), 2000);
The WEBGL_lose_context extension lets you simulate loss and restore in code. Wire it to a debug button so you can verify recovery in dev without waiting for an actual context loss.
Reducing Loss Frequency
- Pause rendering when
document.hiddenis true — less likely to be reclaimed. - Free unused textures explicitly via
gl.deleteTextureinstead of relying on GC. - Avoid creating multiple WebGL contexts on the same page (Canvas + 2D + WebGL counts).
Verifying
Trigger manual context loss with the extension. Verify the game pauses, recovers, and resumes within 1 second. Run a 30-minute soak test in a backgrounded tab; ensure that if the browser reclaims context, your game can recover without reload.
“On WebGL, the context is borrowed. Plan to lose it, plan to recover it — or your game breaks every tab switch.”
Wire up the WEBGL_lose_context test button on every WebGL project. Catch missing rebuilds during development, not in production.