Debugging WebGL: chrome://gpu, Spector.js & Shader Errors
A black canvas with no console errors is the single most common WebGL bug report. WebGL was designed to be a thin, fast layer over the GPU driver — which means it mostly fails silently rather than throwing. This tutorial is the toolkit for actually seeing what the GPU is doing: driver diagnostics, frame capture, correct shader error checks, and context-loss recovery.
1. Why WebGL fails silently
Unlike most JavaScript APIs, calling a WebGL function with the
wrong state rarely throws a catchable exception. A shader that
fails to compile still returns a valid-looking (but unusable)
shader object. A texture bound to the wrong unit doesn't error —
it just samples black, or garbage, or the wrong image. Because
WebGL mirrors OpenGL's C API design, error information sits in an
internal error flag you have to poll
(gl.getError()), not an exception you can catch.
The practical consequence: "nothing rendered" or "it's the wrong colour" can have a dozen different root causes with zero stack trace pointing at any of them. The tools below exist specifically to make that invisible state visible again.
{ failIfMajorPerformanceCaveat: true }
during development. It throws immediately if WebGL would fall
back to a slow, unaccelerated software path — turning a subtle
performance bug into a loud, early failure.
2. chrome://gpu — driver & feature status
Navigate to chrome://gpu in Chrome (or
about:support in Firefox) for a full diagnostic
report before you write a single line of debug code. The
"Graphics Feature Status" table is the first thing to check —
every row should read Hardware accelerated:
| Feature row | If it says "Software only" / "Disabled" |
|---|---|
| WebGL / WebGL2 | All rendering runs on the CPU via SwiftShader — expect 10-50x slower frame times, not a rendering bug in your code. |
| Canvas | 2D canvas operations (readback, texture upload from canvas) get slower too. |
| Rasterization / Compositing |
Indicates a driver blocklist entry — often fixable by
updating GPU drivers or removing a
--disable-gpu-style flag.
|
Below the feature table, the "Problems Detected" section lists exactly which driver bugs Chrome has blocklisted for your specific GPU/driver combination — useful for distinguishing "my shader is wrong" from "this driver has a known bug with this exact extension."
3. Finding the real GPU behind ANGLE
gl.getParameter(gl.RENDERER) on Chrome usually
returns a generic string like
"ANGLE (Google, Vulkan 1.3.0...)" — not useful for
telling which physical GPU is actually running your shaders. The
WEBGL_debug_renderer_info extension exposes the
real values:
const gl = canvas.getContext('webgl2');
const ext = gl.getExtension('WEBGL_debug_renderer_info');
if (ext) {
console.log('Vendor:', gl.getParameter(ext.UNMASKED_VENDOR_WEBGL));
console.log('Renderer:', gl.getParameter(ext.UNMASKED_RENDERER_WEBGL));
} else {
// Extension unavailable — often blocked by a privacy setting or older browser
console.log('Masked renderer:', gl.getParameter(gl.RENDERER));
}
UNMASKED_RENDERER_WEBGL alongside your error
reports and you'll often see a pattern (a specific integrated
GPU, a specific mobile SoC) rather than a code bug affecting
everyone equally.
4. Frame-by-frame capture with Spector.js
Spector.js (browser extension or npm package) records every single WebGL call made during one frame and lets you step through them: bound textures, active shader, uniform values, vertex attribute layout, and the actual framebuffer contents after each draw call.
// npm install --save-dev spectorjs
import { Spector } from 'spectorjs';
const spector = new Spector();
spector.displayUI(); // adds the floating capture button
// or trigger a capture programmatically:
spector.captureCanvas(canvas);
A typical debugging session: capture one frame, then scan the
call list for the draw call that should be rendering your
missing object. Spector.js shows the exact GL state at that
call — if the bound texture thumbnail is blank, you know it's a
texture upload bug; if the vertex count is 0, it's a
bufferData/attribute setup bug; if the shader shows
a compile error icon, jump straight to section 5.
5. Checking shader compile/link errors correctly
This is the single highest-value fix most WebGL codebases are
missing: gl.compileShader() and
gl.linkProgram() never throw and never log
anything by default. A broken shader silently produces an
unusable object unless you explicitly check for it:
function compileShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error(`Shader compile failed:\n${log}`);
}
return shader;
}
function linkProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const log = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw new Error(`Program link failed:\n${log}`);
}
return program;
}
Link errors are a distinct, easy-to-miss failure mode:
compilation can succeed for both shaders individually while
linking still fails — most commonly a
varying mismatch (a fragment shader
in variable with no matching vertex shader
out of the same name and type), or exceeding the
platform's maximum varying/uniform vector count.
ShaderMaterial, compile/link errors
surface in the console automatically in development builds via
renderer.debug.checkShaderErrors. Writing raw WebGL
(as in the code above) is where this checking most often gets
skipped.
6. Handling WebGL context loss
The GPU driver can reclaim a WebGL context at any time — a laptop switching GPUs, another tab exhausting VRAM, the OS recovering from a driver crash. Without a listener, this leaves a permanently black canvas with no error at all:
canvas.addEventListener('webglcontextlost', (e) => {
e.preventDefault(); // required — tells the browser you intend to restore it
cancelAnimationFrame(rafId);
console.warn('WebGL context lost — pausing render loop');
});
canvas.addEventListener('webglcontextrestored', () => {
console.warn('WebGL context restored — reinitialising GL resources');
initGLResources(); // buffers, textures, and programs must all be recreated
requestAnimationFrame(renderLoop);
});
webglcontextrestored
handler. Keep your source data (geometry arrays, image objects,
shader source strings) in plain JS memory rather than only on
the GPU, specifically so this handler has something to rebuild
from.
You can force context loss for testing via the
WEBGL_lose_context extension:
gl.getExtension('WEBGL_lose_context').loseContext() —
essential for verifying your recovery path actually works before
it happens to a real user.
7. Checklist: the usual suspects
| Symptom | Likely cause |
|---|---|
| Nothing renders, no errors | Depth test enabled with nothing clearing the depth buffer, or camera near/far planes excluding the whole scene |
| Object renders solid black | Missing/unbound texture unit, uniform sampler pointing at the wrong texture unit index, or shader compile failure silently returning a default program |
| Geometry looks corrupted/garbled |
Attribute size/type/stride
mismatch with the actual buffer layout, or an
Int16Array vs Uint16Array index
buffer type mismatch
|
| Transparent edges look wrong / halo artefacts | Premultiplied vs. straight alpha mismatch between the texture source and the blend function |
| Works on desktop, breaks on mobile |
mediump/lowp shader precision
overflow, or a WebGL2-only feature used without a
fallback
|
| Flickering between two objects at the same depth | Z-fighting from a near clipping plane set too close to 0 relative to the far plane |
Frequently Asked Questions
What will I learn in this tutorial?
A practical WebGL debugging toolkit: reading chrome://gpu, capturing frames with Spector.js, catching shader compile/link errors properly, handling context loss, and a checklist of the most common silent failures.
What topics are covered in this tutorial?
This tutorial covers: Why WebGL fails silently, chrome://gpu — driver & feature status, Finding the real GPU behind ANGLE, Frame-by-frame capture with Spector.js, Checking shader compile/link errors correctly, Handling WebGL context loss, Checklist: the usual suspects.
How long does this tutorial take?
This tutorial takes approximately 25 minutes to complete.
What prerequisites do I need before starting?
This is a Intermediate-level tutorial — no special preparation beyond basic JavaScript is assumed.