WebGL errors are notoriously silent — a black screen gives you nothing
to go on. This guide covers systematic debugging: polling
gl.getError(), querying renderer info, capturing frames
with Spector.js, profiling GPU timelines, and reducing draw calls for
production performance.
1gl.getError() and shader compilation
errors
WebGL queues errors — call gl.getError() after each
suspect call in development:
function glCheck(gl, label = '') { const err = gl.getError(); if (err
!== gl.NO_ERROR) { const names = { [gl.INVALID_ENUM]: 'INVALID_ENUM',
[gl.INVALID_VALUE]: 'INVALID_VALUE', [gl.INVALID_OPERATION]:
'INVALID_OPERATION', [gl.OUT_OF_MEMORY]: 'OUT_OF_MEMORY', }; throw new
Error(`GL error at "${label}": ${names[err] ?? err}`); } } // Usage:
gl.bindBuffer(gl.ARRAY_BUFFER, buf); glCheck(gl, 'bindBuffer');
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); glCheck(gl,
'bufferData'); // Shader compilation — always check the info log
function compileShader(gl, type, src) { const s =
gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(s)); // <-- line numbers + GLSL
errors throw new Error('Shader compile failed'); } return s; } //
Program link gl.linkProgram(prog); if (!gl.getProgramParameter(prog,
gl.LINK_STATUS)) { console.error(gl.getProgramInfoLog(prog)); //
attribute / varying mismatch info throw new Error('Program link
failed'); }
Wrap gl.getError() calls in a debug mode flag — calling
it every frame forces a CPU/GPU sync and tanks performance. Remove
them or gate behind if (DEBUG) before shipping.
2WEBGL_debug_renderer_info
This extension exposes the actual GPU model string, useful for
identifying driver-specific bugs:
const canvas = document.querySelector('canvas'); const gl =
canvas.getContext('webgl2') ?? canvas.getContext('webgl'); const
dbgInfo = gl.getExtension('WEBGL_debug_renderer_info'); if (dbgInfo) {
const vendor = gl.getParameter(dbgInfo.UNMASKED_VENDOR_WEBGL); const
renderer = gl.getParameter(dbgInfo.UNMASKED_RENDERER_WEBGL);
console.log('GPU vendor: ', vendor); // e.g. "NVIDIA Corporation"
console.log('GPU renderer:', renderer); // e.g. "NVIDIA GeForce RTX
4090/PCIe/SSE2" } else { console.warn('WEBGL_debug_renderer_info not
available (some Firefox configs)'); } // In Three.js: const info =
renderer.extensions.get('WEBGL_debug_renderer_info'); // renderer here
is THREE.WebGLRenderer if (info) { const r =
renderer.getContext().getParameter(info.UNMASKED_RENDERER_WEBGL);
console.log('Three.js GPU:', r); }
Firefox Privacy Resistance mode may return generic strings ("Mozilla"
/ "Mozilla") to prevent GPU fingerprinting. This is a privacy feature,
not a bug.
3Three.js renderer.info
renderer.info provides live render statistics — check it
in your debug overlay:
Navigate to your page, then click the Spector icon in the toolbar
Press Record to capture the next frame
Inspect each draw call: vertex buffers, uniforms, active textures,
depth state
For programmatic integration, load Spector.js as a module:
// Development only — remove from production bundles import * as
SPECTOR from
'https://cdn.jsdelivr.net/npm/spectorjs/dist/spector.bundle.js'; const
spector = new SPECTOR.Spector(); // Capture the next N frames and open
result viewer spector.displayUI(); // floating UI button overlaid on
canvas // or spector.startCapture(canvas, 1); // capture 1 frame
programmatically spector.onCapture.add(result => {
console.log('Spector result JSON:', JSON.stringify(result, null, 2));
});
Check for redundant state changes in Spector —
repeated bindBuffer or useProgram calls with
the same object are a common source of unnecessary CPU overhead.