npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

archetype-ecs

v1.3.3

Published

Lightweight archetype-based Entity Component System

Downloads

439

Readme


An Entity Component System for games and simulations in TypeScript. Entities with the same components are grouped into archetypes, and their fields are stored in TypedArrays — so iterating a million entities is a tight loop over contiguous memory, not a scatter of object lookups.

npm i archetype-ecs
import { createEntityManager, component } from 'archetype-ecs'

const Position = component('Position', 'f32', ['x', 'y'])
const Velocity = component('Velocity', 'f32', ['vx', 'vy'])

const em = createEntityManager()

for (let i = 0; i < 10_000; i++) {
  em.createEntityWith(
    Position, { x: Math.random() * 800, y: Math.random() * 600 },
    Velocity, { vx: Math.random() - 0.5, vy: Math.random() - 0.5 },
  )
}

em.forEach([Position, Velocity], (arch) => {
  const px = arch.field(Position.x)  // Float32Array
  const py = arch.field(Position.y)
  const vx = arch.field(Velocity.vx)
  const vy = arch.field(Velocity.vy)
  for (let i = 0; i < arch.count; i++) {
    px[i] += vx[i]
    py[i] += vy[i]
  }
})

Why archetype-ecs?


Components

import { createEntityManager, component } from 'archetype-ecs'

// Numeric — stored as TypedArrays
const Position = component('Position', 'f32', ['x', 'y'])
const Velocity = component('Velocity', 'f32', ['vx', 'vy'])
const Health   = component('Health', { hp: 'i32', maxHp: 'i32' })

// Strings — stored as arrays, same API
const Name     = component('Name', 'string', ['name', 'title'])

// Mixed — numeric and string fields in one component
const Item     = component('Item', { name: 'string', weight: 'f32' })

// Tag — no data, just a marker
const Enemy    = component('Enemy')

Field types: f32 f64 i8 i16 i32 u8 u16 u32 string

Entities

const em = createEntityManager()

// One at a time
const player = em.createEntity()
em.addComponent(player, Position, { x: 0, y: 0 })
em.addComponent(player, Velocity, { vx: 0, vy: 0 })
em.addComponent(player, Health, { hp: 100, maxHp: 100 })
em.addComponent(player, Name, { name: 'Hero', title: 'Sir' })

// Or all at once
for (let i = 0; i < 10_000; i++) {
  em.createEntityWith(
    Position, { x: Math.random() * 800, y: Math.random() * 600 },
    Velocity, { vx: Math.random() - 0.5, vy: Math.random() - 0.5 },
    Enemy,    {},
  )
}

em.hasComponent(player, Health)   // true
em.removeComponent(player, Health)
em.destroyEntity(player)

Read & write

// Access a single field (doesn't allocate)
em.get(player, Position.x)         // 0
em.get(player, Name.name)          // 'Hero'
em.set(player, Velocity.vx, 5)

// Or grab the whole component as an object (allocates)
em.getComponent(player, Position)  // { x: 0, y: 0 }
em.getComponent(player, Name)      // { name: 'Hero', title: 'Sir' }

Queries — forEach vs query

Two ways to work with entities in bulk. Pick the right one for the job:

forEach — bulk processing

Iterates over matching archetypes. You get the backing TypedArrays directly.

function movementSystem(dt: number) {
  em.forEach([Position, Velocity], (arch) => {
    const px = arch.field(Position.x)  // Float32Array
    const py = arch.field(Position.y)
    const vx = arch.field(Velocity.vx)
    const vy = arch.field(Velocity.vy)
    for (let i = 0; i < arch.count; i++) {
      px[i] += vx[i] * dt
      py[i] += vy[i] * dt
    }
  })
}

query — when you need entity IDs

Returns entity IDs for when you need to target specific entities.

// Find the closest enemy to the player
const enemies = em.query([Position, Enemy])
let closest = -1, minDist = Infinity
for (const id of enemies) {
  const dx = em.get(id, Position.x) - playerX
  const dy = em.get(id, Position.y) - playerY
  const dist = dx * dx + dy * dy
  if (dist < minDist) { minDist = dist; closest = id }
}

// Store the result as a component
em.addComponent(player, Target, { entityId: closest })

// Exclude enemies from friendly queries
const friendly = em.query([Health], [Enemy])

// Just need a count? No allocation needed
const total = em.count([Position])

When to use which

| | forEach | query | |---|---|---| | Use for | Movement, physics, rendering | Damage events, UI, spawning | | Runs | Every frame | On demand | | Allocates | Nothing | number[] of entity IDs | | Access | TypedArrays by field | get / set by entity ID |

Systems

Class-based systems with decorators for component lifecycle hooks:

import { System, OnAdded, OnRemoved, createSystems, type EntityId } from 'archetype-ecs'

class MovementSystem extends System {
  tick() {
    this.forEach([Position, Velocity], (arch) => {
      const px = arch.field(Position.x)
      const py = arch.field(Position.y)
      const vx = arch.field(Velocity.vx)
      const vy = arch.field(Velocity.vy)
      for (let i = 0; i < arch.count; i++) {
        px[i] += vx[i]
        py[i] += vy[i]
      }
    })
  }
}

class DeathSystem extends System {
  @OnAdded(Health)
  onSpawn(id: EntityId) {
    console.log(`Entity ${id} spawned with ${this.em.get(id, Health.hp)} HP`)
  }

  @OnRemoved(Health)
  onDeath(id: EntityId) {
    this.em.addComponent(id, Dead)
  }
}

const em = createEntityManager()
const pipeline = createSystems(em, [MovementSystem, DeathSystem])

// Game loop
em.flushHooks()
pipeline()

@OnAdded(Health, Position) fires when an entity has all specified components. @OnRemoved(Health) fires when any specified component is removed. Hooks are buffered and deduplicated — they fire during pipeline() (or sys.run()), after flushHooks() collects the pending changes.

A functional API is also available:

import { createSystem } from 'archetype-ecs'

const deathSystem = createSystem(em, (sys) => {
  sys.onAdded(Health, (id) => console.log(`${id} spawned`))
  sys.onRemoved(Health, (id) => console.log(`${id} died`))
})

em.flushHooks()
deathSystem()

Serialize

const symbolToName = new Map([
  [Position._sym, 'Position'],
  [Velocity._sym, 'Velocity'],
  [Health._sym, 'Health'],
])

const snapshot = em.serialize(symbolToName)
const json = JSON.stringify(snapshot)

// Later...
em.deserialize(JSON.parse(json), { Position, Velocity, Health })

Supports stripping components, skipping entities, and custom serializers.


TypeScript

Component types are inferred from their definition. Field names autocomplete, wrong fields are compile errors.

// Schema is inferred — Position becomes ComponentDef<'x' | 'y'>
const Position = component('Position', 'f32', ['x', 'y'])

Position.x                // FieldRef — autocompletes to .x and .y
Position.z                // compile error: Property 'z' does not exist

em.get(id, Position.x)    // zero-alloc field access
em.set(id, Position.x, 5) // zero-alloc field write

arch.field(Position.x)    // Float32Array — direct TypedArray access

API reference

component(name)

Tag component — no data, used as a marker for queries.

component(name, type, fields)

Schema component with uniform field type.

const Position = component('Position', 'f32', ['x', 'y'])
const Name     = component('Name', 'string', ['name', 'title'])

component(name, schema)

Schema component with mixed field types.

const Item = component('Item', { name: 'string', weight: 'f32', armor: 'u8' })

createEntityManager()

Returns an entity manager with the following methods:

| Method | Description | |---|---| | createEntity() | Create an empty entity | | createEntityWith(Comp, data, ...) | Create entity with components in one call | | destroyEntity(id) | Remove entity and all its components | | addComponent(id, Comp, data) | Add a component to an existing entity | | removeComponent(id, Comp) | Remove a component | | hasComponent(id, Comp) | Check if entity has a component | | getComponent(id, Comp) | Get component data as object (allocates) | | get(id, Comp.field) | Read a single field | | set(id, Comp.field, value) | Write a single field | | query(include, exclude?) | Get matching entity IDs | | count(include, exclude?) | Count matching entities | | forEach(include, callback, exclude?) | Iterate archetypes with TypedArray access | | onAdd(Comp, callback) | Register callback for component additions (deferred) | | onRemove(Comp, callback) | Register callback for component removals (deferred) | | flushHooks() | Collect pending add/remove events for registered hooks | | serialize(symbolToName, strip?, skip?, opts?) | Serialize world to JSON-friendly object | | deserialize(data, nameToSymbol, opts?) | Restore world from serialized data |

System

Base class for decorator-based systems.

| | Description | |---|---| | @OnAdded(...Comps) | Decorator — method fires when entity gains all specified components | | @OnRemoved(...Comps) | Decorator — method fires when any specified component is removed | | tick() | Override — called every run() after hook callbacks | | forEach(types, callback, exclude?) | Shorthand for this.em.forEach(...) | | run() | Fire buffered hook callbacks, then tick() | | dispose() | Unsubscribe all hooks |

createSystem(em, constructor)

Functional alternative to class-based systems. The constructor receives a context with onAdded, onRemoved, and forEach, and optionally returns a tick function.

createSystems(em, entries)

Creates a pipeline from an array of class-based (System subclasses) and/or functional system constructors. Returns a callable that runs all systems in order, with a dispose() method.


Benchmarks

1M entities, Position += Velocity, 5 runs (median), Node.js:

| | archetype-ecs | bitecs | wolf-ecs | harmony-ecs | miniplex | |---|---:|---:|---:|---:|---:| | Iteration (ms/frame) | 1.7 | 2.2 | 2.2 | 1.8 | 32.5 | | Entity creation (ms) | 401 | 366 | 106 | 248 | 265 | | Memory (MB) | 86 | 204 | 60 | 31 | 166 |

Each library runs the same test — iterate 1M entities over 500 frames:

// archetype-ecs
em.forEach([Position, Velocity], (arch) => {
  const px = arch.field(Position.x)   // Float32Array, dense
  const py = arch.field(Position.y)
  const vx = arch.field(Velocity.vx)
  const vy = arch.field(Velocity.vy)
  for (let i = 0; i < arch.count; i++) {
    px[i] += vx[i]
    py[i] += vy[i]
  }
})

archetype-ecs is fastest at iteration. Harmony-ecs and wolf-ecs are close; miniplex is ~20x slower due to object-based storage.

Run them yourself:

npm run bench

Feature comparison

Compared against other JS ECS libraries:

Unique to archetype-ecs

| Feature | archetype-ecs | bitecs | wolf-ecs | harmony-ecs | miniplex | |---|:---:|:---:|:---:|:---:|:---:| | String SoA storage | ✓ | — | — | — | — | | Mixed string + numeric components | ✓ | — | — | — | — | | forEach with dense TypedArray field access | ✓ | — | — | — | — | | Field descriptors for both per-entity and bulk access | ✓ | — | — | — | — | | TC39 decorator system (@OnAdded / @OnRemoved) | ✓ | — | — | — | — | | Built-in profiler | ✓ | — | — | — | — |

Full comparison

| Feature | archetype-ecs | bitecs | wolf-ecs | harmony-ecs | miniplex | |---|:---:|:---:|:---:|:---:|:---:| | TypedArray iteration | ✓ | ✓ | ✓ | ✓ | — | | String support | ✓ | ✓ | — | — | ✓ | | Serialize / deserialize | ✓ | ✓✓ | — | — | — | | TypeScript type inference | ✓ | — | ✓ | ✓ | ✓✓ | | Batch entity creation | ✓ | — | — | ✓ | ✓ | | Zero-alloc per-entity access | ✓ | ✓ | ✓ | ✓ | — | | System framework (class + functional) | ✓ | — | — | — | — | | Component lifecycle hooks | ✓ | — | — | — | ✓ | | Relations / hierarchies | — | ✓ | — | — | — | | React integration | — | — | — | — | ✓ |

✓✓ = notably stronger implementation in that library.

archetype-ecs is the only one that combines fast iteration, string storage, serialization, decorator-based systems, and type safety.


License

MIT