# Script performance: writing behaviours that stay cheap

Behaviours run on QuickJS, an interpreter. A call into the engine (`ctx.entity.position`, a
`ctx.world.query` result's `.position`, `ctx.events.emit`) costs far more than the arithmetic around
it, and every `[x, y, z]` a helper returns is an allocation. A behaviour that does a little work for
each of many things can cost milliseconds a frame without doing anything expensive. This page is
what to write instead. **Every pattern here uses the API as it is; none changes what a game does.**

**In one line:** in a hot `update`, ask the engine for the answer (`query({ near, sort: "distance",
limit: 1 })`, `ctx.world.positions(list)`, `ctx.events.emitTo`) rather than reading every handle, and
keep vector maths in number locals.

## Find what costs first

`GET /profile/frames` (`crates/query/API.md`) names the biggest behaviour call of each frame
(`top_behaviour`) and each frame's `script_ms`; `GET /profile/trace` opens the frames in Perfetto,
one span per behaviour call. Measure before rewriting: a behaviour that runs once a second is not
worth touching.

## The patterns, biggest saving first

| Instead of | Write | Why |
|---|---|---|
| `query({ near, radius })`, then reading `.position` of every result to find the nearest | `query({ near, radius, sort: "distance", limit: 1 })[0]` | The engine sorts; the script reads one handle, not every one. Each result handle and each `.position` read is an engine call |
| `for (const f of foes) f.position` | `const ps = ctx.world.positions(foes)` (one `Float32Array`, 3 numbers each) | One call for the whole list |
| `ctx.events.emit("hit", { to })` with every receiver checking `to` | `ctx.events.emitTo(target, "hit", data)` | `emit` reaches every behaviour: N senders and N receivers is N × N handler calls |
| one `update` per instance of a behaviour placed on hundreds of entities | `updateAll(ctx, dt, batch)` (`docs/API.md`, "One call for every instance") | One host call a tick instead of one per instance |
| `add(scale(sub(b, a), t), a)` in a per-frame loop | `const x = a[0] + (b[0] - a[0]) * t` (and y, z) in number locals | Each helper returns a new array; locals allocate nothing |
| reading `ctx.entity.position` several times in one call | read it once into a local | Every read is an engine call and a new array |

## What it costs (measured)

Release build, one CPU core of the Triton workstation (wave117), `crates/app/tests/script_cost.rs`.
Microseconds per behaviour call; lower is better. These are this machine's numbers, for comparing
patterns, not a budget.

| Behaviour | Idiomatic (allocating helpers) | Number locals | Number locals, no engine calls | `updateAll` |
|---|---|---|---|---|
| sway, 24 objects (4 engine calls each) | 3.8 | 2.6 | 1.0 | 2.0 per object |
| spring follow, 8 followers | 4.2 | 2.7 | | |
| kart-like controller, 8 karts (~32 vector operations) | 9.6 | 4.9 | | |

One engine call costs about 0.4-0.7 µs; one allocating vector operation about 0.15 µs.

### A worked case: 200 bots, each finding its nearest enemy

`fixtures/netcode/arena-200`'s `combat.js` queries the enemies within 30 m of each bot every tick and
reads every result's position to find the nearest (about 75 results a query). The same game, rewritten
one pattern at a time (median of three runs; ms of `combat.update` a tick for all 200 bots):

| `combat.js` | ms a tick | engine calls a tick |
|---|---|---|
| as checked in: `query({ near, radius })`, `.position` of each result | 10.9 | 16,883 |
| the results' positions in one `ctx.world.positions(foes)` | 8.1 | 2,165 |
| `query({ near, radius, sort: "distance", limit: 1 })` | 2.1 | 2,166 |
| that, and `emitTo(best, …)` instead of `emit` | 2.1 | 839 |

Asking the engine for the nearest one is five times cheaper than finding it in the script: each
result of a query is a handle the engine builds for the script, so a query that answers 75 handles
costs 75 builds even when the script only wants one.

