🌾 FarmHeart
← Back to Blog

Testing and Debugging Browser Games: A Developer’s Toolkit

October 6, 2026 · FarmHeart · 10 min read

Why Game Debugging Is Different

Browser games run in a rendering loop at 60 frames per second. A bug that fires once every 200 frames — a physics glitch, a memory leak, a dropped input — won’t show up in a unit test. Traditional web debugging tools help, but you need game-specific techniques layered on top.

This guide covers the practical toolkit: Chrome DevTools for performance, Spector.js for WebGL, structured logging for game state, and automated testing strategies that actually work for real-time games.

Chrome DevTools: Your First Line of Defense

The Performance Tab

The Performance tab records a timeline of everything the browser does — JavaScript execution, rendering, compositing, and garbage collection. For games, the key metric is frame time: anything over 16.67ms means you dropped below 60fps.

Record 5–10 seconds of gameplay, then look for:

The Memory Tab

Take heap snapshots before and after a gameplay session. Compare them to find objects that should have been garbage collected but weren’t. Common culprits in games:

Pro tip: Use performance.mark() and performance.measure() to instrument specific game systems. You can then see custom markers in the DevTools timeline.
// Instrument your update loop
performance.mark('physics-start');
this.physics.step(dt);
performance.mark('physics-end');
performance.measure('Physics', 'physics-start', 'physics-end');

performance.mark('render-start');
this.renderer.draw();
performance.mark('render-end');
performance.measure('Render', 'render-start', 'render-end');

Structured Game Logging

Raw console.log is noisy in a game loop. Instead, build a structured logger that categorizes messages and can be filtered:

const GameLog = {
  _filters: new Set(['physics', 'input', 'ai', 'render']),
  _enabled: true,

  log(system, msg, data) {
    if (!this._enabled || !this._filters.has(system)) return;
    console.log(`[${system.toUpperCase()}] ${msg}`, data || '');
  },

  // Toggle in console: GameLog.toggle('physics')
  toggle(system) {
    this._filters.has(system)
      ? this._filters.delete(system)
      : this._filters.add(system);
  }
};

// Usage
GameLog.log('physics', 'Collision detected', { a: entity1.id, b: entity2.id });
GameLog.log('ai', 'NPC state change', { npc: 'farmer', from: 'idle', to: 'harvest' });

Expose GameLog on window during development so you can toggle systems from the browser console without reloading.

WebGL Debugging with Spector.js

When something renders wrong — missing textures, incorrect colors, z-fighting — you need to inspect the actual WebGL draw calls. Spector.js is a browser extension that captures a single frame and shows you every WebGL command, shader, texture, and buffer.

What to look for in a Spector.js capture:

Common Browser Game Bugs

Memory Leaks

The most insidious game bug. Your game runs fine for 5 minutes, then starts stuttering. The usual pattern:

  1. Entity is “destroyed” but still referenced somewhere (an array, a callback, a physics body)
  2. Each destroyed entity keeps its textures, meshes, and audio buffers alive
  3. After hundreds of entities, the heap grows until GC pauses cause visible stutters

Fix: Use a proper entity lifecycle. When an entity is destroyed, explicitly remove it from every system — physics world, render list, event listeners, particle emitters. Consider an object pool for frequently created/destroyed objects like bullets or particles.

requestAnimationFrame Gotchas

// BAD: dt will spike to seconds after tab switch
function update(timestamp) {
  const dt = (timestamp - lastTime) / 1000;
  lastTime = timestamp;
  physics.step(dt); // dt = 30 seconds after returning from another tab
  requestAnimationFrame(update);
}

// GOOD: clamp dt to prevent physics explosion
function update(timestamp) {
  const raw = (timestamp - lastTime) / 1000;
  const dt = Math.min(raw, 0.1); // cap at 100ms
  lastTime = timestamp;
  physics.step(dt);
  requestAnimationFrame(update);
}

When users switch tabs and come back, requestAnimationFrame resumes with a massive delta time. Without clamping, physics objects teleport through walls and timers skip ahead.

WebGL Context Loss

Mobile browsers and some desktop GPUs will kill your WebGL context under memory pressure. If you don’t handle it, the game shows a black screen with no error message.

canvas.addEventListener('webglcontextlost', (e) => {
  e.preventDefault(); // allows context restore
  cancelAnimationFrame(rafId);
  showMessage('Restoring graphics...');
});

canvas.addEventListener('webglcontextrestored', () => {
  // Recreate all GL resources
  initShaders();
  reuploadTextures();
  rebuildBuffers();
  rafId = requestAnimationFrame(update);
});

Unit Testing Game Logic

You can’t easily unit-test rendering, but you can test game logic — and you should. The key is separating logic from rendering:

// game-logic.js — pure functions, no DOM/canvas
export function calculateDamage(attacker, defender) {
  const base = attacker.strength * attacker.weapon.multiplier;
  const reduced = Math.max(0, base - defender.armor);
  const crit = attacker.critChance > Math.random() ? 2 : 1;
  return Math.round(reduced * crit);
}

export function canPlantCrop(tile, crop, season) {
  if (tile.type !== 'soil') return false;
  if (tile.occupied) return false;
  if (!crop.seasons.includes(season)) return false;
  return true;
}

// game-logic.test.js
test('damage reduced by armor', () => {
  const atk = { strength: 10, weapon: { multiplier: 1.5 }, critChance: 0 };
  const def = { armor: 5 };
  expect(calculateDamage(atk, def)).toBe(10); // (10*1.5)-5 = 10
});

Run these with any test runner (Vitest, Jest). They execute in milliseconds and catch regression bugs before they reach the canvas.

Mobile Debugging

Mobile browser games break in unique ways — touch event ordering, viewport resizing, GPU differences. Use Chrome’s remote debugging:

  1. Connect your Android device via USB
  2. Enable USB debugging in Android developer settings
  3. Open chrome://inspect on your desktop Chrome
  4. Click “Inspect” next to your device’s tab — full DevTools, running against the real mobile GPU

For iOS, use Safari’s Web Inspector (Develop menu → your device). Test on real hardware — iOS Safari’s WebGL implementation has quirks that no emulator reproduces.

A Debugging Checklist

SymptomFirst CheckTool
Stuttering / frame dropsGC pauses or long JS executionDevTools Performance tab
Black screenWebGL context loss or shader errorSpector.js + context event listeners
Wrong colors / missing objectsTexture binding or draw orderSpector.js frame capture
Increasing memory usageEntity lifecycle / disposed resourcesDevTools Memory snapshots
Physics explosion after tab switchUnclamped delta timeStructured logging on dt values
Touch not registeringpreventDefault / passive listenersChrome remote debugging

Build and Debug with FarmHeart

See these techniques in action in a real browser game.

Play FarmHeart