askill
game-perf

game-perfSafety 95Repository

Optimize game code for per-frame performance and GC pressure. Use PROACTIVELY when editing game loops, update functions, render code, or any code that runs every frame. Identifies allocation anti-patterns and provides zero-allocation alternatives.

1 stars
1.2k downloads
Updated 2/13/2026

Package Files

Loading files...
SKILL.md

Game Performance Optimization

This skill provides patterns for writing allocation-free, GC-friendly code in game loops and hot paths. Apply these patterns proactively when working on any code that executes per-frame.

When to Activate

Trigger this skill when editing:

  • Game loops, update functions, tick handlers
  • Render/draw functions
  • Physics update code
  • AI/behavior update code
  • Collision detection
  • Particle systems
  • Any function called 60+ times per second

Anti-Patterns and Fixes

1. Spread Operator Copies

Problem: Spread creates a new array every call.

// BAD: Creates new array every frame
const context = {
  enemies: [...this.enemies],
  projectiles: [...this.projectiles],
};

Fix: Pass readonly references.

// GOOD: Zero allocation
const context = {
  enemies: this.enemies as readonly EnemyState[],
  projectiles: this.projectiles as readonly ProjectileState[],
};

2. Array.filter() in Hot Paths

Problem: filter() always creates a new array.

// BAD: New array every call
const activeEnemies = enemies.filter(e => e.active);

Fix: In-place filtering with swap-and-truncate.

// GOOD: Mutate in place
function filterInPlace<T>(array: T[], predicate: (item: T) => boolean): void {
  let writeIndex = 0;
  for (let i = 0; i < array.length; i++) {
    if (predicate(array[i])) {
      array[writeIndex++] = array[i];
    }
  }
  array.length = writeIndex;
}

3. Array.map() for Transformations

Problem: map() creates a new array.

// BAD: New array every frame
const positions = enemies.map(e => e.worldPos);
steering.separation(ctx, positions, radius);

Fix: Scratch array or inline iteration.

// GOOD: Reuse scratch array
const positionsScratch: Vec2[] = [];

function getPositions(enemies: readonly EnemyState[]): readonly Vec2[] {
  positionsScratch.length = 0;
  for (const e of enemies) {
    positionsScratch.push(e.worldPos);
  }
  return positionsScratch;
}

4. Filter + Map Chains

Problem: Double allocation.

// BAD: Two new arrays
const activePositions = enemies
  .filter(e => e.active)
  .map(e => e.worldPos);

Fix: Single-pass with scratch array.

// GOOD: Single pass, zero allocation
const scratch: Vec2[] = [];
function getActivePositions(enemies: readonly EnemyState[]): readonly Vec2[] {
  scratch.length = 0;
  for (const e of enemies) {
    if (e.active) scratch.push(e.worldPos);
  }
  return scratch;
}

5. Returning New Arrays from Utilities

Problem: Helper functions that return new arrays per call.

// BAD: New array per entity per frame
function getWrappedPositions(pos: Vec2): Vec2[] {
  const positions = [pos];
  // ... add wrapped positions
  return positions;
}

Fix: Module-level scratch with readonly return.

// GOOD: Reusable scratch buffer
const scratchPositions: Vec2[] = [];

function getWrappedPositions(pos: Vec2): readonly Vec2[] {
  scratchPositions.length = 0;
  scratchPositions.push(pos);
  // ... add wrapped positions
  return scratchPositions;
}

The readonly return type signals to callers: "consume immediately, do not store."

6. O(n²) Proximity Queries

Problem: Checking every entity against every other entity.

// BAD: O(n²) - checks all enemies for each enemy
for (const enemy of enemies) {
  const nearby = enemies.filter(e =>
    e !== enemy && distance(e.pos, enemy.pos) < radius
  );
}

Fix: Spatial hash grid for O(n) build + O(1) queries.

// GOOD: Build grid once, query many times
const grid = new Map<string, Entity[]>();
const CELL_SIZE = 100;

function buildGrid(entities: readonly Entity[]): void {
  grid.clear();
  for (const e of entities) {
    const key = `${Math.floor(e.pos.x / CELL_SIZE)},${Math.floor(e.pos.y / CELL_SIZE)}`;
    if (!grid.has(key)) grid.set(key, []);
    grid.get(key)!.push(e);
  }
}

function queryNearby(pos: Vec2, radius: number): readonly Entity[] {
  scratch.length = 0;
  const cx = Math.floor(pos.x / CELL_SIZE);
  const cy = Math.floor(pos.y / CELL_SIZE);
  // Check 3x3 cells
  for (let dx = -1; dx <= 1; dx++) {
    for (let dy = -1; dy <= 1; dy++) {
      const cell = grid.get(`${cx + dx},${cy + dy}`);
      if (cell) {
        for (const e of cell) {
          if (distance(e.pos, pos) < radius) scratch.push(e);
        }
      }
    }
  }
  return scratch;
}

7. Object Creation in Loops

Problem: Creating temporary objects inside loops.

// BAD: New object per iteration
for (const enemy of enemies) {
  const ctx = { position: enemy.pos, velocity: enemy.vel };
  updateAI(ctx);
}

Fix: Reuse a single context object.

// GOOD: Reuse context object
const ctx = { position: { x: 0, y: 0 }, velocity: { x: 0, y: 0 } };

for (const enemy of enemies) {
  ctx.position.x = enemy.pos.x;
  ctx.position.y = enemy.pos.y;
  ctx.velocity.x = enemy.vel.x;
  ctx.velocity.y = enemy.vel.y;
  updateAI(ctx);
}

Architecture Patterns

Build Once, Query Many

// Per-frame setup phase
buildSpatialGrid(entities);
buildEnemyGrid(enemies);

// Per-entity query phase (many times)
for (const entity of entities) {
  const nearby = queryNearby(entity.pos, RADIUS);
  // process nearby...
}

Readonly Signals Transience

When a function returns a readonly array, it communicates:

  • The array is a scratch buffer
  • Caller must consume immediately
  • Do not store the reference
  • Contents will change on next call

Object Pooling for Frequent Create/Destroy

For entities created/destroyed frequently (particles, projectiles):

class Pool<T> {
  private available: T[] = [];

  acquire(factory: () => T): T {
    return this.available.pop() ?? factory();
  }

  release(item: T): void {
    this.available.push(item);
  }
}

Checklist for Hot Path Code

Before committing changes to per-frame code:

  • No spread operators ([...array]) on arrays that don't change
  • No filter() / map() / reduce() creating new arrays
  • No object literals ({}) or array literals ([]) inside loops
  • Proximity queries use spatial partitioning if > 50 entities
  • Scratch arrays used for temporary results
  • Return types are readonly for scratch buffers
  • Context objects are reused, not recreated

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

94/100Analyzed 2/24/2026

High-quality technical skill for game performance optimization. Provides detailed, actionable anti-patterns with concrete TypeScript solutions for reducing GC pressure in game loops. Includes 'When to Activate' section, 7 distinct allocation patterns with code examples, architecture patterns, and a practical checklist. The only minor issue is the mismatched 'ci-cd' tag - more appropriate tags would be 'performance', 'game-dev', or 'gc'. Overall excellent reference material for game developers.

95
95
90
95
95

Metadata

Licenseunknown
Version-
Updated2/13/2026
Publisherrbergman

Tags

ci-cd