Skip to main content
ClaudeWave
Install in Claude Code
Copy
git clone --depth 1 https://github.com/gamedev-skills/awesome-gamedev-agent-skills /tmp/performance-optimization && cp -r /tmp/performance-optimization/skills/disciplines/performance-optimization ~/.claude/skills/performance-optimization
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# Performance optimization

Performance work is a measurement discipline, not a bag of tricks. The method is always the
same: **profile → find the one bottleneck → fix that → measure again**. This skill teaches that
loop and the highest-leverage fixes (pooling, batching, allocation control, asset budgets), and
points you at each engine's profiler. It pairs with `physics-tuning` for simulation cost.

## When to use

- Use when the frame rate is low or uneven, the game stutters/hitches, or it must hit a target
  (60 FPS desktop, 30/60 mobile) and currently doesn't.
- Use to decide *what* to optimize: profile, read the frame budget, and identify whether the CPU
  or GPU is the bottleneck before changing any code.
- Use to apply specific fixes: object pooling, draw-call/batch reduction, removing per-frame
  allocations and GC spikes, and setting asset budgets.

**When *not* to use:** for physics jitter/tunneling/timestep specifically, use `physics-tuning`.
For the engine's concrete profiler UI and rendering settings, use that
engine skill (`godot-export` covers some build settings; engine cores cover the rest). This skill
is the cross-engine method and the shared fixes.

## The golden rule: measure first, never guess

Most performance "fixes" applied without profiling target the wrong thing and add complexity for
no gain. **Do not optimize code you have not measured.** Open the profiler, find the single
biggest cost in a representative scene on representative hardware, and fix that. Re-measure to
confirm the fix helped before moving on. Profile a **release/optimized build** where it matters —
editor and debug builds lie (editor overhead, no compiler optimization).

## Core workflow

1. **Define the target and reproduce.** State the goal (e.g. 60 FPS = 16.67 ms/frame) and find a
   repeatable worst-case scene. "Sometimes slow" is unfixable; a reproducible spike is fixable.
2. **Profile before touching code.** Run the engine profiler and read the frame: total frame
   time, and the split between CPU (game logic, physics, scripts) and GPU (rendering).
3. **Find the bottleneck — CPU or GPU.** If GPU time ≫ CPU, attack draw calls/overdraw/shaders/
   resolution. If CPU time dominates, attack scripts/physics/allocations. Fixing the wrong side
   does nothing.
4. **Fix the single biggest cost.** Prefer an **algorithmic** win (do less work, cache, spatial
   partition, run less often) over micro-optimizing a hot line. Apply the matching shared fix
   (pooling, batching, allocation removal).
5. **Re-measure on the same scene/hardware.** Confirm the number moved. Keep or revert based on
   data, not intuition.
6. **Set budgets so it stays fixed.** Per-frame ms budgets per subsystem, plus asset budgets
   (texture sizes, triangle counts, draw-call ceilings); add a perf check to verification.
7. **Report measured numbers.** State before/after frame time, the bottleneck found, and the fix
   — never "should be faster". If you could only measure in-editor, say so.

## Patterns

### 1. Frame budget math (turn "feels slow" into a number)

```text
target FPS → frame budget:   60 FPS = 16.67 ms   |   30 FPS = 33.3 ms   |   120 FPS = 8.33 ms
The WHOLE frame (CPU sim + render submit + GPU) must fit the budget; the GPU runs in parallel,
so the slower of CPU-frame and GPU-frame sets your FPS. Allocate sub-budgets, e.g. @60 FPS:
  gameplay/scripts ~5 ms · physics ~3 ms · rendering(CPU submit) ~4 ms · UI/other ~2 ms · slack.
If one subsystem blows its slice, that's your target — not whatever you assumed.
```

### 2. Measure with the engine profiler (do this before any fix)

```text
Godot 4.7 : Debugger ▸ Profiler (script/physics time) and Monitors tab (FPS, draw calls, memory).
            In code: Performance.get_monitor(Performance.TIME_PROCESS) and
            Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME).
Unity 6.3 LTS   : Profiler window (CPU/GPU/Memory/Rendering modules) + Frame Debugger for draw calls.
            In code: a ProfilerRecorder tracking "CPU Main Thread Frame Time" for a HUD/log.
Unreal 5  : `stat unit` (Frame/Game/Draw/GPU ms), `stat fps`, `stat scenerendering` (draw calls);
            Unreal Insights for deep traces.
# Read the split: is the Draw/GPU line the biggest, or the Game/CPU line? That decides the fix.
```

### 3. Object pooling (stop allocating/freeing in hot loops)

```gdscript
# Bullets, particles, enemies, damage numbers: reuse a fixed set instead of instantiate()/free()
# every frame — that thrashes memory and (in C#) feeds the GC.
var _pool: Array[Node] = []
func acquire() -> Node:
    var n: Node = _pool.pop_back() if not _pool.is_empty() else bullet_scene.instantiate()
    n.set_process(true); n.visible = true
    return n
func release(n: Node) -> void:
    n.set_process(false); n.visible = false       # disable + hide; DON'T free
    _pool.append(n)                                # back to the pool for reuse
# RIGHT: pre-warm the pool at load; reuse. WRONG: instantiate()/queue_free() per shot.
```

### 4. Cut draw calls (the most common GPU-side win)

```text
Each unique material/texture/state change is roughly a draw call; thousands of them stall the GPU.
- Atlas textures and share materials so sprites/meshes batch into one call.
- Identical meshes → GPU instancing (Unity), MultiMesh / MultiMeshInstance (Godot), Instanced
  Static Mesh (Unreal).
- Static geometry → static batching / baking; mark non-moving objects static.
- Reduce overdraw: limit large overlapping transparent/particle layers (they re-shade pixels).
- Fewer real-time lights/shadows; bake lighting where it doesn't move.
Measure draw calls before and after — the count should drop, and so should GPU frame time.
```

### 5. Kill per-frame allocations (GC spikes = stutter)

```csharp
// Unity 6.3 LTS (C#). Allocating every frame fills the managed heap; the GC then stalls a frame.
// WRONG (allocates each call): foreach (var e in FindObjectsOfType<Enemy>()) ...  // + LINQ, new[]
// RIGHT: