Three.js From Zero · Article s14-06

Debugging with Spector.js

Debugging with Spector.js is Article s14-06 of Three.js From Zero, a MasterAllArts free interactive lesson for artists learning creative 3D on the web.

← Three.js From ZeroS14-06 · Setup & Tooling

Season 14 · Article 06 · Setup & Tooling

When something looks wrong in your 3D scene and you can't tell why, Spector.js captures every WebGL call for a single frame and lets you inspect it. Like Chrome DevTools but for the GPU pipeline. The tool that finds the leak / the mis-bound buffer / the duplicated draw.

Install

// As a Chrome extension — install from Chrome Web Store, click the icon, click "capture"
// Or as a script tag for headless capture:
<script src="https://unpkg.com/spectorjs"></script>
<script>new SPECTOR.Spector().displayUI();</script>

The extension is faster to use but requires a browser. The library route works in iframes and lets you capture programmatically.

What a captured frame shows

Click "capture frame" while your scene runs. Spector freezes one frame and shows:

  • Every WebGL call in order: gl.useProgram, gl.bindBuffer, gl.uniformMatrix4fv, gl.drawElements...
  • Render targets at each draw call — you see what was being drawn to.
  • Shader source for the program in use (with line numbers).
  • Uniform values at each draw.
  • Texture contents bound at the time.

Diagnose: "FPS is bad"

Count the draw calls. Spector tells you at the top: "X draw calls, Y triangles." Anything over 200 draw calls on a static scene means you're not batching enough — see S1-09 instancing.

Diagnose: "Why is this black?"

Find the draw call for the broken mesh in Spector. Inspect the bound texture — is it black? The uniform — is the color set right? The shader — does it run for that material?

Specific pattern: a mesh that should be visible isn't appearing. In Spector, search for its draw call. If absent, your scene graph filtered it out (visibility, layers, frustum culling). If present but rendered to a wrong target, your render order is broken.

Diagnose: "Performance dropped after refactor"

Capture frames before and after. Diff the draw counts. The cause is usually one of:

  • Material got new uniforms → broke material caching → shader recompiles per frame.
  • Instanced mesh became plural meshes → 100 draws instead of 1.
  • Shadow maps doubled in size → 4× the shadow-pass time.

Other tools (when Spector isn't enough)

  • renderer.info — programmatic stats per frame: renderer.info.calls, .triangles, .points, .lines, .programs.length, .geometries, .textures. Log these in your animation loop.
  • Chrome Performance tab — flamegraph of CPU time. Find your JS hot loops.
  • Chrome Memory tab — heap snapshots. Find leaked geometries.
  • WebGL Inspector — alternative to Spector, slightly different feature set.

Three.js-specific quick checks

// In your loop, every 60 frames:
if (frame % 60 === 0) {
  console.log('Calls:', renderer.info.render.calls,
              'Tris:', renderer.info.render.triangles,
              'Programs:', renderer.info.programs.length);
}

If calls climbs over time without a reason — you're leaking objects. If programs.length climbs — you're creating new materials per frame (don't).

Common first-time pitfalls

"Spector says shader recompiles every frame." You're mutating a material property that triggers a recompile (e.g., adding a new attribute, changing onBeforeCompile output). Cache the modified material.
"renderer.info.geometries grows forever." Disposal isn't running. After scene changes, you need geometry.dispose() + material.dispose() manually. Three.js doesn't garbage-collect GPU resources.
"Spector can't capture my canvas." Canvas is in an iframe or shadow DOM. Inject the library directly inside the iframe.

Exercises

  1. Profile your biggest scene. Capture a frame. Note draw call count. Optimize to halve it — usually via instancing or merging similar materials.
  2. Track renderer.info over time. Log every 60 frames. Watch for monotonic growth — that's a leak.
  3. Find a slow shader. Capture in Spector. Look at the fragment shader for your most-drawn material. Count operations. Aim for under 100 ops in the fragment shader for any mesh that covers significant screen area.