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.6.0

Published

Lightweight archetype-based Entity Component System

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.

WASM SIMD

em.apply sets a field to the result of an expression — no loops, no raw arrays:

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

em.apply(Position.x, add(Position.x, Velocity.vx))   // px += vx
em.apply(Position.y, add(Position.y, Velocity.vy))   // py += vy
em.apply(Velocity.vx, scale(Velocity.vx, 0.99))      // friction

Available expressions:

add(a, b)        // a[i] + b[i]
sub(a, b)        // a[i] - b[i]
mul(a, b)        // a[i] * b[i]
scale(a, s)      // a[i] * s

The required components are inferred from the expression — no query needed. When WASM SIMD is available and the fields are f32, this runs 4x faster than a manual JS loop. Otherwise it falls back to scalar JS automatically.

Use forEach + field() for custom operations that can't be expressed as simple math:

em.forEach([Position, Velocity], (arch) => {
  const vy = arch.field(Velocity.vy)
  for (let i = 0; i < arch.count; i++)
    vy[i] = Math.max(vy[i] - 9.81 * dt, -50)   // gravity + terminal velocity
})

When does SIMD kick in?

| Condition | Check | Fallback | |---|---|---| | Runtime supports WASM SIMD | Tested once at startup by compiling a 925-byte SIMD module | All operations use scalar JS | | WASM mode not disabled | createEntityManager() (default) or { wasm: true } | createEntityManager({ wasm: false }) forces JS-only | | Field type is f32 | apply checks if arrays are Float32Array | Scalar JS loop |

WASM SIMD is supported in all modern browsers (Chrome 91+, Firefox 89+, Safari 16.4+) and Node.js 16+.

import { isWasmSimdAvailable } from 'archetype-ecs'

isWasmSimdAvailable()                      // true if runtime supports SIMD
createEntityManager({ wasm: false })       // force JS-only mode

How SIMD acceleration works

Regular JavaScript processes one float at a time. When you write px[i] += vx[i] on a Float32Array, V8 converts each value from f32 to f64 and back — that's the only float precision JS supports natively.

WASM SIMD uses f32x4.add: a single CPU instruction that adds 4 floats in parallel, directly in 32-bit precision. For 1M entities, that's 250K instructions instead of 1M, with no conversion overhead.

Storage layout

When WASM mode is active, all numeric TypedArrays (Float32Array, Int32Array, etc.) are allocated on a shared WebAssembly.Memory via a bump allocator. This means the SIMD kernel operates directly on the data — no copying between JS and WASM. String fields always use regular JS arrays.

  • The arena reserves 128 MB virtual address space (lazily committed — no physical RAM cost on most OSes)
  • The bump allocator doesn't reclaim memory — frequent archetype churn may waste space

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(options?)

Returns an entity manager. WASM SIMD is auto-detected and enabled by default. Pass { wasm: false } to force JS-only mode.

| 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 | | apply(target, expr) | Set a field to an expression result — SIMD-accelerated for f32 | | 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 | The forEach callback receives an ArchetypeView with:

| Method | Description | |---|---| | field(ref) | Get the backing TypedArray for a field | | fieldStride(ref) | Elements per entity (1 for scalars, N for arrays) | | snapshot(ref) | Get the snapshot TypedArray (change tracking) |

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 | |---|---:|---:|---:|---:|---:| | Iterationapply() (ms/frame) | 0.29 | 1.6 | 1.4 | 1.1 | 28.9 | | Iterationfield() + loop (ms/frame) | 1.2 | — | — | — | — | | Entity creation (ms) | 501 | 359 | 105 | 255 | 157 | | Memory (MB) | 86+128 | 204 | 60 | 31 | 166 |

Each library iterates 1M entities over 500 frames (Position += Velocity):

// apply() — declarative, SIMD-accelerated
em.apply(Position.x, add(Position.x, Velocity.vx))
em.apply(Position.y, add(Position.y, Velocity.vy))

// forEach + field() — manual loop for custom operations
em.forEach([Position, Velocity], (arch) => {
  const vy = arch.field(Velocity.vy)
  for (let i = 0; i < arch.count; i++)
    vy[i] = Math.max(vy[i] - 9.81 * dt, -50)
})

Run them yourself:

npm run bench                                            # vs other ECS libraries
node --expose-gc bench/wasm-iteration-bench.js           # WASM SIMD benchmark

Feature comparison

Compared against other JS ECS libraries:

Unique to archetype-ecs

| Feature | archetype-ecs | bitecs | wolf-ecs | harmony-ecs | miniplex | |---|:---:|:---:|:---:|:---:|:---:| | WASM SIMD iteration (auto-detected) | ✓ | — | — | — | — | | 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