Tutorial · All Levels · ~40 min
WebGL · Three.js · Spector.js · DevTools · GPU Profiling

Debugging WebGL — DevTools, Spector.js & Profiling

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:

// After renderer.render(scene, camera): const i = renderer.info; console.table({ 'Draw calls': i.render.calls, // major performance metric 'Triangles': i.render.triangles, 'Points': i.render.points, 'Lines': i.render.lines, 'Geometries': i.memory.geometries, // GPU-memory objects 'Textures': i.memory.textures, 'Programs': i.programs?.length ?? '–', // compiled shaders }); // HUD overlay (useful during development) const hud = document.createElement('div'); Object.assign(hud.style, { position:'fixed', bottom:'10px', left:'10px', color:'#4ade80', fontFamily:'monospace', fontSize:'12px', background:'rgba(0,0,0,.5)', padding:'6px 10px', borderRadius:'4px', pointerEvents:'none', zIndex:'9999' }); document.body.appendChild(hud); function updateHUD() { hud.textContent = `DC: ${renderer.info.render.calls} | Tri: ${renderer.info.render.triangles}`; renderer.info.reset(); // reset per-frame counters }

4Spector.js — frame capture and inspection

Spector.js is a browser extension that captures a full WebGL frame and lets you inspect every draw call, uniform, and texture 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.

5Chrome GPU timeline profiling

// CPU-side frame timing with Performance API let t0 = performance.now(); renderer.render(scene, camera); // Note: this does NOT include GPU time — WebGL calls return immediately. // Use EXT_disjoint_timer_query for true GPU timing: const ext = gl.getExtension('EXT_disjoint_timer_query_webgl2'); if (ext) { const query = gl.createQuery(); gl.beginQuery(ext.TIME_ELAPSED_EXT, query); renderer.render(scene, camera); gl.endQuery(ext.TIME_ELAPSED_EXT); // Poll result on a future frame (non-blocking) setTimeout(() => { const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE); if (available) { const ns = gl.getQueryParameter(query, gl.QUERY_RESULT); console.log('GPU frame time:', (ns / 1e6).toFixed(2), 'ms'); } }, 100); }

6Draw call reduction techniques

import { BufferGeometryUtils } from 'three/examples/jsm/utils/BufferGeometryUtils.js'; // Merge 1000 trees into a single draw call const merged = BufferGeometryUtils.mergeGeometries( trees.map(t => { const g = t.geometry.clone(); g.applyMatrix4(t.matrixWorld); // bake world transform return g; }) ); scene.add(new THREE.Mesh(merged, treeMat)); trees.forEach(t => scene.remove(t)); // remove individual meshes

Frequently Asked Questions

What will I learn in this tutorial?

Learn to debug WebGL and Three.js applications: browser GPU timeline, WEBGL_debug_renderer_info, Spector.js frame capture, error checking, and draw call reduction.

What topics are covered in this tutorial?

This tutorial covers: gl.getError() and GL error helper, WEBGL_debug_renderer_info, Three.js renderer.info, Spector.js — frame capture and inspection, Chrome GPU timeline profiling, Draw call reduction techniques.

What tools and technologies does this tutorial use?

This tutorial uses WebGL, Three.js, Spector.js, DevTools, GPU Profiling.

How long does this tutorial take?

This tutorial takes approximately 40 minutes to complete.