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

@byloth/micro-ecs

v1.0.35

Published

A simple & lightweight ECS (Entity Component System) library for JavaScript and TypeScript. 🕹

Readme

μECS 🕹

NPM Version Codecov NPM release GPR release NPM Downloads License

A simple & lightweight ECS (Entity Component System) library for JavaScript and TypeScript.


Overview

μECS (micro-ecs) is a headless library, completely agnostic to any graphics or rendering library.

Traditional ECS architectures excel in low-level contexts with direct memory control (C++, Rust, etc.).
JavaScript doesn't offer this level of control, so μECS takes a pragmatic approach: it brings only the ECS benefits that translate well to a high-level environment, leaving behind optimizations that would require direct memory management.

Design Philosophy

The library is built on three pillars:

  • DX-first: Developer Experience is prioritized over raw performance.
    The library doesn't use TypedArrays which would be faster but significantly hurt ergonomics.
    A pleasant API is more valuable than squeezing out every microsecond.
  • Familiarity: The API should feel natural to any JavaScript developer.
    This means using recognizable patterns: ES6 classes, getters/setters, pub/sub events, and typical OOP idioms common in the JS ecosystem.
  • Speed over Memory: When trade-offs are necessary, execution speed is preferred over memory consumption.
    Using extra memory is acceptable if it yields performance benefits at runtime.

Is it any good?

Yes. [1]


Installation

# npm
npm install @byloth/micro-ecs @byloth/core

# pnpm
pnpm add @byloth/micro-ecs @byloth/core

# yarn
yarn add @byloth/micro-ecs @byloth/core

# bun
bun add @byloth/micro-ecs @byloth/core

[!NOTE] This library requires @byloth/core as a peer dependency.


Quick Start

import { World, Component, System } from "@byloth/micro-ecs";
import type { ReadonlyQueryView } from "@byloth/micro-ecs";

// Define Components
class Position extends Component {
  public x = 0;
  public y = 0;
}
class Velocity extends Component {
  public vx = 0;
  public vy = 0;
}

// Define a System
class MovementSystem extends System {
  private _view!: ReadonlyQueryView<[Position, Velocity]>;

  public override initialize(world: World): void {
    super.initialize(world);

    this._view = world.getComponentView(Position, Velocity);
  }
  public override update(deltaTime: number): void {
    for (const [position, velocity] of this._view.components) {
      position.x += velocity.vx * deltaTime;
      position.y += velocity.vy * deltaTime;
    }
  }
}

// Create the World
const world = new World();

// Add Systems
world.addSystem(new MovementSystem());

// Create Entities and Components
const entity = world.createEntity();
entity.createComponent(Position);
entity.createComponent(Velocity);

// Game loop
let _lastTime = performance.now();
function gameLoop() {
  const currentTime = performance.now();
  const deltaTime = currentTime - _lastTime;

  world.update(deltaTime);

  _lastTime = currentTime;

  requestAnimationFrame(gameLoop);
}

requestAnimationFrame(gameLoop);

Key Concepts

Lifecycle

Every core class follows the Poolable pattern: the constructor creates the object in an empty state, initialize() activates it, and dispose() resets it.
Entities and Components are pooled automatically by the World.

World

The central container. Creates and destroys Entities, registers Systems and Resources, dispatches events, and drives the update loop.

const world = new World();

// Entities
const entity = world.createEntity();
world.destroyEntity(entity);

// Systems (priority order — lower runs first)
world.addSystem(new PhysicsSystem());
world.addSystem(new RenderSystem(10));
world.removeSystem(PhysicsSystem);

// Resources (singletons shared across the world)
world.addResource(new GameConfig());
world.removeResource(GameConfig);

// Services (System + Resource in one)
world.addService(new InputManager());
world.removeService(InputManager);

// Events
world.emit("player:hit", entity, 10);

// Update loop & cleanup
world.update(deltaTime);
world.dispose();

Entity & Component

Entities are containers for Components. Each entity can hold at most one instance of a given Component type.
Both can be enabled/disabled to show or hide them from queries.

const entity = world.createEntity();

const hp = entity.createComponent(Health, true, 200);
entity.hasComponent(Health);  // true
entity.getComponent(Health);  // Health
entity.destroyComponent(Health);

entity.disable();  // Hides from queries
entity.enable();   // Visible again

Components accept custom parameters through initialize():

class Health extends Component {
  public current = 100;
  public max = 100;

  public override initialize(entity: Entity, enabled?: boolean, maxHp?: number): void {
    super.initialize(entity, enabled);

    this.current = maxHp ?? 100;
    this.max = maxHp ?? 100;
  }
}

System

Systems contain logic that runs every frame via update().
They extend Resource and support priority-based execution order.

class PhysicsSystem extends System {
  public constructor() {
    super(/* priority */ 0, /* enabled */ true);
  }

  public override initialize(world: World): void {
    super.initialize(world);
  }
  public override update(deltaTime: number): void {
    // Called every frame by world.update()
  }
  public override dispose(): void {
    super.dispose();
  }
}

Queries & Views

The World exposes methods to query entities by component types.
getComponentView() returns a cached, auto-updating ReadonlyQueryView.

// One-shot queries
world.getFirstComponent(Position);
world.getFirstComponents(Position, Velocity);

// Lazy iteration
for (const [pos, vel] of world.findAllComponents(Position, Velocity)) { /* ... */ }

// Cached view (preferred in Systems)
const view = world.getComponentView(Position, Velocity);

view.size;         // number
view.has(entity);  // boolean
view.get(entity);  // [Position, Velocity] | undefined

for (const [position, velocity] of view.components) {
  position.x += velocity.vx;
}

// React to changes
view.onAdd((entity, components) => { /* ... */ });
view.onRemove((entity, components) => { /* ... */ });
view.onClear(() => { /* ... */ });

Contexts

A WorldContext gives Systems access to events and resource dependencies.
An EntityContext lets Components declare dependencies on sibling Components.

// WorldContext — obtained in System.initialize()
const ctx = world.getContext(this);

ctx.on("player:hit", handler);    // Subscribe
ctx.once("game:start", handler);  // Subscribe once
ctx.off("player:hit", handler);   // Unsubscribe
await ctx.wait("player:hit");     // Async wait

const config = ctx.useResource(GameConfig);  // Declare dependency
ctx.releaseResource(GameConfig);             // Release dependency

// EntityContext — obtained in Component.initialize()
const ctx = entity.getContext(this);

const pos = ctx.useComponent(Position);  // Require sibling
ctx.releaseComponent(Position);          // Release dependency

[!NOTE] A component with active dependants cannot be destroyed until its dependants are removed first.


Roadmap

🔴 Critical Implementations

Issues that block or compromise production usage.

Luckily, none at the moment.


🟠 Known Bugs & Limitations

Known issues and current limitations to be aware of.

Inexplicably, none at the moment.


🟡 Improvements

Optimizations and refinements that improve quality and performance.

  • [ ] Automatic View garbage collection

    Implement an automatic clean-up system for QueryManager that detects and removes Views no longer referenced or used, avoiding memory accumulation over time.


🟢 Future Considerations

Ideas and possible evolutions to evaluate based on needs.

  • [ ] Archetypes

    Consider an archetype system to group entities with the same component "signature", optimizing queries.

    ⚠️ In a JavaScript context, the traditional benefits of archetypes (cache locality, contiguous memory) are not exploitable.
    The cached View system already present in QueryManager covers part of these advantages. Actual utility to be evaluated.


🔵 Nice to Have

Features that would be beneficial but are not critical.

  • [ ] Advanced Query System

    Rewrite the query system from scratch to allow users to define queries using, chaining and nesting logical operators: and, or and not.


License

Apache 2.0