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:
- JavaScript execution: game logic, physics, AI, input handling
- Rendering: draw calls, canvas/WebGL operations
- Browser overhead: layout, paint, compositing (minimal for canvas games, significant for DOM games)
- GC pauses: garbage collection can cause frame spikes
If your total exceeds the budget, frames drop. The player sees stuttering.
Chrome DevTools Performance Panel
The most powerful profiling tool you already have:
- Open DevTools (F12) → Performance tab
- Click Record, play your game for 5-10 seconds of the problematic area, click Stop
- The flame chart shows exactly what ran in each frame and how long it took
- Look for: long yellow bars (JavaScript), long green bars (rendering), red triangles (dropped frames)
Key Metrics
- Frame duration: hover over frames in the chart. Anything over 16ms at 60 FPS target is a problem.
- Scripting time: the JavaScript portion. If this dominates, your game logic is too expensive.
- Rendering time: the GPU/paint portion. If this dominates, you have too many draw calls or overdraw.
- Idle time: unused budget. If you see idle time but still drop frames, the problem is intermittent (GC, layout thrashing).
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
- Each
ctx.drawImage() call has overhead. Batch sprites from the same texture together.
- For tile maps, pre-render static layers to an off-screen canvas once, then draw the cached canvas each frame.
- Use
ctx.getImageData() and ctx.putImageData() sparingly — they force a GPU-to-CPU round trip.
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:
- Heap snapshot: take snapshots before and after a game session. Compare to find memory leaks (objects that grow but never shrink).
- Allocation timeline: shows when objects are allocated. Look for allocations that happen every frame.
- Common leaks: event listeners not removed, references to destroyed entities kept in arrays, closures capturing large scopes.
WebGL-Specific Profiling
- Spector.js: browser extension that captures WebGL frames and shows every draw call, shader, texture, and state change. Essential for WebGL games.
- Draw call count: keep it under 100 for most browser games. Each draw call has CPU overhead regardless of how few pixels it draws.
- Texture switches: changing the active texture is expensive. Atlas your sprites into fewer, larger textures.
- Shader complexity: fragment shaders run for every pixel. A complex shader on a full-screen quad is much more expensive than the same shader on a small sprite.
Optimization Priorities
Always profile before optimizing. The rules of thumb:
- Algorithmic fixes first: O(n²) to O(n log n) beats any micro-optimization. Spatial hashing for collision, view frustum culling, dirty rectangles.
- Reduce work: don’t process what you can’t see. Skip updates for off-screen, sleeping, or unchanged entities.
- Batch and cache: pre-render static content, batch draw calls, pool objects.
- 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