Tutorial · WebGL · Debugging · Tools
📅 July 2026 ⏱ ≈ 25 min 🎯 Intermediate

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.

Cheapest first step: create your context with { 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));
}
Why it matters for bug reports: "black screen on some users' machines" is almost always GPU/driver-specific. Log 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.

Also worth knowing: Chrome DevTools' own WebGL Inspector panel (via the "More tools" menu in recent Chrome versions, or the classic standalone "WebGL Inspector" extension for older workflows) offers similar call-list inspection built into the browser, without installing anything into your page's bundle.

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.

Three.js already does this for you: if you're using Three.js's 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);
});
Everything GPU-side is gone after loss — every buffer, texture, and compiled shader program must be recreated from scratch inside the 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
Next steps: pair this with the mobile WebGL optimisation tutorial for the precision and driver quirks specific to phones and tablets, or the WebGL Extensions reference for the full list of debug/diagnostic extensions beyond the two used here.

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.