@zakkster/lite-ecs
v1.0.0
Published
Zero-GC Entity Component System built on monomorphic bitwise masks and preallocated object pools.
Maintainers
Readme
@zakkster/lite-ecs
A zero-GC Entity Component System engineered for 60fps browser games. Built on top of pre-allocated object pools and silicon-speed bitwise filtering.
The ECS architecture that prevents frame drops and memory leaks.
Why lite-ecs?
Most JavaScript game loops rely on OOP (class Player extends Sprite) and object dictionary lookups (if (obj.velocity) { ... }). This creates constant garbage collection (GC) spikes and destroys CPU cache locality.
lite-ecs solves this by fusing two high-performance primitives:
lite-object-pool— Entities are never created or destroyed during gameplay. They are acquired and released from a pre-allocated stack. Zero allocations.@zakkster/lite-fastbit32— Systems filter entities using O(1) branchless bitwise operations. Zero dictionary lookups.
| Feature | lite-ecs | Standard JS OOP |
| ------------------------ | ---------------------------- | ---------------------------- |
| Memory footprint | Flat, pre-allocated | Fragmented |
| System filtering | O(1) bitwise mask | O(N) dictionary lookups |
| Mid-frame death guard| ✅ Automatic | ❌ Prone to null-ref crashes |
| Level transitions | ✅ Zero-GC world.clear() | ❌ Massive GC spikes |
| Reentrancy guard | ✅ Built in | ❌ Prone to stack overflows |
| Bundle size | < 3KB gzipped | N/A |
Learn the Architecture
Want to know exactly how this engine works under the hood?
The full Lite ECS Masterclass & Handbook walks you through every design decision. You will learn data-oriented design, how to implement FastBit32 state flags, build pre-allocated Object Pools, and architect your own Zero-GC game engine from scratch.
👉 Get the Masterclass & Handbook on itch.io
Installation
npm install @zakkster/lite-ecsQuick Start
import { World, Entity, System } from '@zakkster/lite-ecs';
// 1. Define your flat data shell
class GameEntity extends Entity {
constructor() {
super();
this.x = 0; this.y = 0;
this.vx = 0; this.vy = 0;
}
reset() {
super.reset();
this.x = 0; this.y = 0;
this.vx = 0; this.vy = 0;
}
}
// 2. Write pure-logic systems
class PhysicsSystem extends System {
constructor() { super(['Position', 'Velocity']); }
update(entity, dt) {
entity.x += entity.vx * dt;
entity.y += entity.vy * dt;
}
}
// 3. Boot the world
const world = new World({
components: ['Position', 'Velocity'],
entityFactory: () => new GameEntity(),
maxEntities: 5000
});
world.addSystem(new PhysicsSystem());
// 4. Spawn and tick
const bullet = world.spawn();
bullet.vx = 200;
bullet.addComponent(world.C('Position'))
.addComponent(world.C('Velocity'));
world.tick(1 / 60);API Reference
World
The orchestrator. Manages memory and executes systems.
| Method | Description |
| ------------------- | --------------------------------------------------------------------------- |
| addSystem(sys) | Mounts a system. Throws if called after the first tick(). |
| spawn() | Acquires a clean entity from the pool. Returns null if the pool is capped.|
| destroy(entity) | Releases an entity back to the pool instantly. |
| C('String') | Translates a component string name into its raw integer bit. |
| tick(dt) | Executes all systems against all active entities. Reentrancy-safe. |
| clear() | Releases all active entities and resets systems. Perfect for level changes. |
System
Pure logic blocks.
| Method | Description |
| ------------------------ | ---------------------------------------------------------------------------- |
| constructor(['Names']) | Pass an array of required component strings to subscribe the system. |
| update(entity, dt) | Override this. Runs once per matching entity per frame. |
| reset() | Optional override. Called when world.clear() is triggered. |
Entity
The zero-GC data shell. You must extend this class to hold your custom fields.
| Method | Description |
| ------------------------- | --------------------------------------------------------------------------- |
| addComponent(bit) | Attaches a component flag. Expects the integer from world.C(). |
| removeComponent(bit) | Removes a component flag. |
| reset() | Override this. Called automatically on release. Always call super.reset().|
Recipes
Never iterate arrays calling delete when the player beats a level — it causes frame drops. Use clear() to instantly wipe the board without re-allocating memory.
function onLevelComplete() {
// 1. Releases all 5,000 active bullets and enemies back to the pool
// 2. Calls reset() on all Systems to clear their internal state
world.clear();
// Load next level — zero allocations, zero GC pressure
loadLevel(2);
}You can dynamically change how systems interact with an entity by toggling its bits.
class PowerupSystem extends System {
constructor() { super(['Player', 'Invincible']); }
update(entity, dt) {
entity.invincibleTimer -= dt;
if (entity.invincibleTimer <= 0) {
// Instantly un-subscribes the player from this system!
entity.removeComponent(world.C('Invincible'));
}
}
}If DamageSystem kills an entity earlier in the frame, RenderSystem running later is guaranteed to skip it. The alive flag is checked automatically by the tick loop.
class DamageSystem extends System {
constructor(world) {
super(['Health']);
this.world = world;
}
update(entity) {
if (entity.hp <= 0) this.world.destroy(entity); // entity.alive = false
}
}
// Any later system in this frame will skip this entity.Learn More
The full ECS Handbook walks you through every design decision and teaches you to build a Zero-GC game architecture from scratch.
License
MIT — Zahary Shinikchiev
