FarmHeart

Performance · Profiling · Optimization · 10 min read · October 8, 2026

Performance Profiling for Browser Games

Performance is the difference between a game that feels responsive and one that feels sluggish. Browser games run in a shared environment — competing with other tabs, extensions, and the browser itself for CPU, GPU, and memory. Profiling tells you exactly where your frame time goes so you fix the actual bottleneck instead of guessing.

The Frame Budget

At 60 FPS, you have 16.67ms per frame. At 30 FPS (acceptable for slower games), you have 33.33ms. Your frame budget breaks down into:

If your total exceeds the budget, frames drop. The player sees stuttering.

Chrome DevTools Performance Panel

The most powerful profiling tool you already have:

  1. Open DevTools (F12) → Performance tab
  2. Click Record, play your game for 5-10 seconds of the problematic area, click Stop
  3. The flame chart shows exactly what ran in each frame and how long it took
  4. Look for: long yellow bars (JavaScript), long green bars (rendering), red triangles (dropped frames)

Key Metrics

In-Game Performance Monitor

class PerfMonitor {
  constructor() {
    this.frameTimes = [];
    this.lastTime = 0;
    this.el = document.createElement('div');
    this.el.style.cssText = 'position:fixed;top:4px;left:4px;background:rgba(0,0,0,.7);' +
      'color:#0f0;font:12px monospace;padding:4px 8px;z-index:9999;pointer-events:none';
    document.body.appendChild(this.el);
  }

  begin(now) { this.frameStart = now; }

  end(now) {
    const dt = now - this.lastTime;
    this.lastTime = now;
    this.frameTimes.push(dt);
    if (this.frameTimes.length > 60) this.frameTimes.shift();

    const avg = this.frameTimes.reduce((a, b) => a + b) / this.frameTimes.length;
    const fps = 1000 / avg;
    const worst = Math.max(...this.frameTimes);
    this.el.textContent = \`FPS: \${fps.toFixed(0)} | Avg: \${avg.toFixed(1)}ms | Worst: \${worst.toFixed(1)}ms\`;
  }
}

Common Performance Killers

Garbage Collection Spikes

Creating objects every frame triggers GC pauses. The fix is object pooling:

// BAD: creates a new object every frame
function update() {
  const velocity = { x: speed * Math.cos(angle), y: speed * Math.sin(angle) };
  entity.x += velocity.x;
}

// GOOD: reuse a pre-allocated object
const _vel = { x: 0, y: 0 };
function update() {
  _vel.x = speed * Math.cos(angle);
  _vel.y = speed * Math.sin(angle);
  entity.x += _vel.x;
}

Excessive Draw Calls

Unculled Entities

Don’t update or render entities that are off-screen:

function isVisible(entity, camera) {
  return entity.x + entity.w > camera.x &&
         entity.x < camera.x + camera.w &&
         entity.y + entity.h > camera.y &&
         entity.y < camera.y + camera.h;
}

// In your render loop
for (const entity of entities) {
  if (isVisible(entity, camera)) {
    entity.render(ctx);
  }
}

Memory Profiling

DevTools Memory panel shows:

WebGL-Specific Profiling

Optimization Priorities

Always profile before optimizing. The rules of thumb:

  1. Algorithmic fixes first: O(n²) to O(n log n) beats any micro-optimization. Spatial hashing for collision, view frustum culling, dirty rectangles.
  2. Reduce work: don’t process what you can’t see. Skip updates for off-screen, sleeping, or unchanged entities.
  3. Batch and cache: pre-render static content, batch draw calls, pool objects.
  4. Micro-optimize last: typed arrays, bit manipulation, and inlining matter only after the big wins are taken.

Related Articles

Ready to Play?

Jump into FarmHeart — free, no download, runs in your browser.

Play FarmHeart