High-Performance gamedev techniques

ECS Architecture, Object Pooling, Prototypal Inheritance

// Pattern 01

Entity-Component
System Architecture

The Entity-Component-System (ECS) pattern is arguably the most impactful architectural decision you can make in a JavaScript game. It inverts the classical OOP model: instead of objects carrying behaviour and data together, everything is decomposed into three orthogonal concepts.

Concept A

Entity

A bare numeric ID — nothing more. No methods, no data. Think of it as a key in a database. Thousands can exist with near-zero memory cost.

Concept B

Component

Pure, flat data structs attached to entities. Position, Velocity, Sprite, Health. No logic whatsoever — just plain JavaScript objects.

Concept C

System

Functions that iterate over entities matching a component signature and transform their data. All game logic lives here, nowhere else.

// ECS Data Flow
World
Entity 42
Position
Velocity
Sprite
MovementSystem
queries
Position + Velocity
updates pos

The real power emerges from query-based composition. A MovementSystem operates on every entity that has both Position and Velocity. Freeze an entity? Add a Frozen tag component. The system skips it automatically. No inheritance tree to untangle.

ecs-world.js
// ── Minimal ECS World ───────────────────────────────
class World {
  constructor() {
    this.nextId     = 0;
    this.components = new Map();  // Map<type, Map<entityId, data>>
    this.systems    = [];
  }

  createEntity() { return this.nextId++; }

  addComponent(entity, type, data) {
    if (!this.components.has(type))
      this.components.set(type, new Map());
    this.components.get(type).set(entity, data);
  }

  query(...types) {
    // Return entity IDs that own ALL requested component types
    const [first, ...rest] = types;
    const candidates = [...(this.components.get(first)?.keys() ?? [])];
    return candidates.filter(id =>
      rest.every(t => this.components.get(t)?.has(id))
    );
  }

  get(entity, type) {
    return this.components.get(type)?.get(entity);
  }

  tick(dt) {
    for (const system of this.systems) system(dt, this);
  }
}

// ── Systems are plain functions ──────────────────────
const MovementSystem = (dt, world) => {
  for (const id of world.query('Position', 'Velocity')) {
    const pos = world.get(id, 'Position');
    const vel = world.get(id, 'Velocity');
    pos.x += vel.x * dt;
    pos.y += vel.y * dt;
  }
};

// ── Scene setup ─────────────────────────────────────
const world = new World();
world.systems.push(MovementSystem);

const player = world.createEntity();
world.addComponent(player, 'Position', { x: 0, y: 0 });
world.addComponent(player, 'Velocity', { x: 120, y: 0 });
world.addComponent(player, 'Sprite',   { sheet: 'player.png' });
// Performance note

For cache-coherent access in hot loops, store components in typed Float32Arrays indexed by entity ID instead of nested Maps. Libraries like bitecs do exactly this, often hitting 10× throughput improvements on large entity counts.

Scenario ECS Deep OOP
10 000 moving entities Fast Slow
Adding new behaviour Add component + system Refactor hierarchy
Serialisation / save state Trivial — plain data Complex
Initial learning curve Medium Low
// Pattern 02

Object Pooling

The garbage collector is a game developer's silent enemy. Every new Bullet() inside your game loop is a ticking time bomb — the GC will eventually pause your frame to clean it up, causing stutters even on 60 Hz targets. Object pooling pre-allocates a fixed set of objects and reuses them, keeping allocation entirely out of the critical path.

// Bullet pool — 24 slots (12 active, 12 free)
Active (in-flight)
Free (pooled)

The pool hands out a dormant object when you fire a bullet, resets its state, and marks it active. On impact or exit, the object is returned to the free list — no memory freed, no allocation needed.

object-pool.js
class ObjectPool {
  constructor(factory, initialSize = 64) {
    this.factory  = factory;
    this.free     = [];
    this.active   = new Set();

    // Pre-warm the pool
    for (let i = 0; i < initialSize; i++)
      this.free.push(this.factory());
  }

  acquire() {
    const obj = this.free.pop() ?? this.factory(); // grow if empty
    this.active.add(obj);
    return obj;
  }

  release(obj) {
    if (!this.active.has(obj)) return;
    obj.reset();
    this.active.delete(obj);
    this.free.push(obj);
  }

  releaseAll() {
    for (const obj of this.active) this.release(obj);
  }
}

// ── Bullet factory ──────────────────────────────────
const bulletPool = new ObjectPool(() => ({
  x: 0, y: 0, vx: 0, vy: 0,
  damage: 0, active: false,
  reset() {
    this.x = this.y = this.vx = this.vy = 0;
    this.active = false;
  }
}), 256);

// ── Usage inside the game loop ───────────────────────
function fireBullet(x, y, angle, spd) {
  const b  = bulletPool.acquire();
  b.x      = x;
  b.y      = y;
  b.vx     = Math.cos(angle) * spd;
  b.vy     = Math.sin(angle) * spd;
  b.active = true;
  b.damage = 10;
}

function updateBullets(dt) {
  for (const b of bulletPool.active) {
    b.x += b.vx * dt;
    b.y += b.vy * dt;
    if (isOffScreen(b)) bulletPool.release(b);
  }
}
// Common pitfall

Never hold a reference to a pooled object after calling release(). The pool will hand it to someone else, and you'll be mutating a live object — a class of bug that's extremely hard to track down.

Pooling shines most for frequently spawned, short-lived objects: bullets, particles, explosions, damage numbers, audio clips. For long-lived, few-in-number objects the overhead isn't worth it. Profile first.

Object TypePool?Reason
Bullets / projectilesYesHigh frequency, short lifespan
Particle effectsYesHundreds per second
Audio buffer nodesYesWebAudio node creation is expensive
Level entities (trees)NoLong-lived, infrequently spawned
UI elementsMaybeOnly in long scroll lists
// Pattern 03

Prototypal
Inheritance

Unlike class-based languages, JavaScript objects inherit directly from other objects via the prototype chain. Understanding this mechanism — not just using the class sugar on top of it — unlocks powerful game-specific patterns around shared behaviour, mixin composition, and ultra-fast method lookup.

◆ GameObject.prototype
Entity.prototype
Player.prototypeadds jump(), attack()
Enemy.prototypeadds pathfind(), aggro()
Projectile.prototype
Bullet.prototype
Missile.prototypeoverrides update()

The class keyword in ES6 is syntactic sugar — under the hood, class Foo extends Bar still wires up Foo.prototype.__proto__ === Bar.prototype. What makes prototypal inheritance uniquely powerful is runtime mutability: you can add a method to a prototype and every existing instance immediately gains it, with zero per-instance cost.

game-objects.js
// ── Raw prototypal style (explicit) ────────────────
const gameObjectProto = {
  update(dt) { /* base tick */ },
  destroy()  { this.active = false; }
};

const entityProto = Object.create(gameObjectProto);
entityProto.move = function(dx, dy) {
  this.x += dx; this.y += dy;
};

// ── Factory: no `new`, no class keyword ─────────────
function createPlayer(x, y) {
  const player = Object.create(entityProto);
  player.x      = x;
  player.y      = y;
  player.hp     = 100;
  player.active = true;

  player.jump = function() {
    this.vy = -400;
  };

  return player;
}

// ── Mixin for shared behaviours ──────────────────────
const damageable = {
  takeDamage(amount) {
    this.hp -= amount;
    if (this.hp <= 0) this.destroy();
  }
};

const shieldable = {
  takeDamage(amount) {
    const blocked = Math.min(this.shield, amount);
    this.shield -= blocked;
    damageable.takeDamage.call(this, amount - blocked);
  }
};

// Apply mixin at runtime
Object.assign(entityProto, damageable);

// Upgrade specific instance
const boss = createPlayer(512, 300);
Object.assign(boss, shieldable, { hp: 1000, shield: 250 });

// ── Class syntax (same prototype wiring, cleaner) ───
class Bullet extends GameObject {
  constructor(x, y, vx, vy) {
    super(x, y);
    this.vx = vx; this.vy = vy;
  }

  update(dt) {
    this.x += this.vx * dt;
    this.y += this.vy * dt;
    if (isOffScreen(this)) this.destroy();
  }
}
// V8 engine insight

V8 (and SpiderMonkey) assign hidden classes to objects. When all instances share the same shape — same properties added in the same order — method dispatch is monomorphic and JIT-compiled to near-native speed. Factory functions and Object.create() make this trivial to guarantee; ad-hoc property additions break it.

// ECS vs. Prototype Inheritance

Deep prototype chains (5+ levels) hinder JIT optimisation and complicate debugging. For games with highly variable entity types, ECS composition beats deep inheritance every time. Use prototype chains for shallow, stable hierarchies (3 levels max) and reach for ECS when you need maximum flexibility.