Keep a Three.js game at a smooth framerate as it grows — instancing, level-of-detail, object pooling, culling, draw-call and allocation discipline, and profiling with vitexec. Use when a game stutters or drops frames, or proactively when a scene has many objects, enemies, particles, or lights.
--- name: performance description: "Keep a Three.js game at a smooth framerate as it grows — instancing, level-of-detail, object pooling, culling, draw-call and allocation discipline, and profiling with vitexec. Use when a game stutters or drops frames, or proactively when a scene has many objects, enemies, particles, or lights." --- # Performance A scene that runs fine with a few objects stutters once it has crowds, particles, and a big map. The framerate is set by three budgets — **draw calls**, **GPU work**, and **per-frame CPU/allocations** — and the levers below each target one. Measure first, then fix the actual bottleneck. ## Measure first Don't guess. Profile the running game with `vitexec --performance-trace` (and `--cpu-profile` for hot functions). Read the verdict from where the frame time goes: long scripting/JS time per frame and a busy main thread mean **CPU-bound** (apply "less per-frame CPU"); a cheap main thread but high GPU/render time, many draw calls, or large overdraw mean **GPU-bound** (apply "less GPU work" / "fewer draw calls"). Fix the budget the trace actually points to, not the one you assumed. ## Fewer draw calls Each distinct mesh is roughly one draw call; thousands of them stall the CPU submitting work. - **Instancing**: render many copies of the same geometry (trees, rocks, grass, bullets, identical enemies) as one `InstancedMesh`. Position each copy with `mesh.setMatrixAt(i, matrix)` and then set `mesh.instanceMatrix.needsUpdate = true` — omit that flag and instances never move. - **Merge static geometry**: combine non-moving meshes that share a material into one buffer geometry. The tradeoff: a merged mesh is culled all-or-nothing, so don't merge geometry spread across a large area that benefits from per-object frustum culling. - **Share materials/textures**: reuse material and texture instances; an atlas lets different props share one draw call. ## Less GPU work - **Cull**: leave `frustumCulled = true` (the default) so off-screen objects are skipped; for huge worlds, also cull by distance. - **LOD**: swap distant objects for cheaper versions (`THREE.LOD`), or hide small distant detail entirely. - **Overdraw**: cap large transparent/additive particles and full-screen effects; bound shadow-map size and the number of shadow-casting lights. ## Less per-frame CPU - **Pool, don't allocate**: reuse objects, and reuse scratch `Vector3`/`Quaternion`/`Matrix4` instances instead of `new`-ing them inside the update loop — per-frame allocation causes GC hitches. - **Cap dynamic counts**: bound the number of live enemies, particles, and projectiles; recycle from a pool rather than creating/destroying. - **Don't trigger recompiles**: changing the light count or a material's defines at runtime recompiles shaders (see the `lights` skill). Keep them fixed. - **Spread work**: amortize expensive systems (pathfinding, AI re-planning) across frames instead of doing all of it every frame.
Creator's repository · drawcall-ai/skills