# bitECS leaves the architecture to you, and the data layout to your arrays

> bitECS is a TypeScript entity component system whose distinguishing feature is that it does not decide much: components can be any shape you like, from plain arrays to typed arrays to objects per entity, and systems are ordinary functions you call. That flexibility is the product, and so is the consequence that nothing in the library protects you from the wrong layout.

**NateTheGreatt/bitECS** —  Flexible, minimal, data-oriented ECS library for Typescript

- Repository: https://github.com/NateTheGreatt/bitECS
- Website: https://bitecs.dev
- Stars: 1,527 · Forks: 119
- Language: TypeScript
- License: MPL-2.0
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/natethegreatt-bitecs

## An ECS that refuses to be opinionated

bitECS is described as a flexible, minimal, data-oriented ECS library for TypeScript, and the README expands that into a claim about what it deliberately does not do. It is a minimal, less opinionated and powerful Entity Component System library that provides a lean API enabling developers to build their architecture to their liking, offering flexibility while maintaining efficiency where needed.

That is the whole pitch, and it cuts both ways. An opinionated engine gives you a scene graph, a scheduler, a transform hierarchy, an asset pipeline and a set of conventions, and you get all of it for free and you cannot easily replace any of it. A minimal library gives you the primitives and the conventions, and you assemble the rest.

The feature table makes the trade explicit. A simple, declarative API and lightweight, around 5kb minzipped. Powerful querying and zero dependencies. Relational entity modeling, thread-friendly, and serialization included.

The audience is therefore a developer who already has opinions about architecture, or a team migrating from an object oriented engine where the framework's assumptions did not fit. The project topics list includes game development, game engine and high performance, and the README names three projects as built on it: the Enchantment Engine, Third Room and Hubs.

The licence is MPL-2.0, a file level copyleft, and the package is published to npm as bitecs at version 0.4.0. The repository is not archived, and the last push was on 2026-08-24, with no GitHub releases, so the version on npm is the version to pin.

## Components are whatever shape you want

The design decision that makes bitECS different from most ECS libraries is visible in the example, and it is that a component is not a class.

The components are declared when the world is created, and they can be any shape you want:

```typescript
    Position: { x: [], y: [] },
    Velocity: { x: new Float32Array(1e5), y: new Float32Array(1e5) },
    // AoS:
    Player: [] as { level: number; experience: number; name: string }[]
```

The comment in the example labels the two access patterns, and they are the classic ones. The first is a structure of arrays, where each field of the component is its own array indexed by entity id, and the second is an array of structures, where each entity has one object holding all its fields. The library supports both, and the choice is yours per component.

The Velocity line shows why the type matters. Using a Float32Array of a hundred thousand entries for the velocity means the memory is contiguous, the values are single precision, and there is no per object allocation. Using a plain array of objects for the player, where each entity holds a level, an experience and a name, is comfortable to work with and costs an allocation per entity.

The access patterns follow the layout, and the example shows both. The SoA style writes into the field arrays by entity id, while the AoS style writes a whole object at once:

```typescript
  Position.x[eid] = 0
  Position.y[eid] = 0
  Velocity.x[eid] = 1.23
  Velocity.y[eid] = 1.23
  Health[eid] = 100
```

and later, for the player:

```typescript
  Player[eid] = { level: 1, experience: 0, name: "Hero" }
```

The flexibility is the feature, and the cost is that nothing here stops you from putting the position of ten thousand entities into an array of objects and wondering why the frame time is bad. In a framework that fixed the layout for you, that mistake was impossible.

## Queries, systems and a loop you own

The parts of an ECS that remain are the parts that matter.

Entities are integers. addEntity(world) returns an entity id, addComponent(world, eid, Component) attaches a component to it, and removeEntity(world, eid) takes it out. A query selects the entities that have a given set of components, and the iteration is over ids:

```typescript
  for (const eid of query(world, [Position, Velocity])) {
    Position.x[eid] += Velocity.x[eid] * world.time.delta
    Position.y[eid] += Velocity.y[eid] * world.time.delta
  }
```

That is a system in the sense that matters: a function that takes the world and updates entities. The example defines several of them, a movement system, an experience system that increments a player's experience and levels them up at a threshold, a health system that removes entities whose health drops to zero, and a time system that computes the delta from the previous frame and accumulates the elapsed time.

Then there is the update function, which calls the systems in order:

```typescript
setInterval(() => {
  update(world)
}, 1000/60)
```

for a Node environment, and a requestAnimationFrame loop for the browser. That is the shape of the whole library in one line: the engine is yours, the frame loop is yours, and the systems are plain functions you call in the order you want.

The practical consequence is that there is no dependency injection between systems, no automatic ordering, and no built-in notion of a frame. For a small project that is a feature, because the whole update path is readable in one screen. For a large project it is a decision you will have to revisit, because ordering and scheduling do not appear on their own.

The world also carries the time state, with a delta, an elapsed accumulator and the timestamp of the last frame, and the example's systems read the delta from there rather than calling a clock themselves. The creation call takes the components and the initial time state, so a world is a small plain object you can pass around.

## Serialization, legacy and the shape of the published package

The package manifest shows that bitECS publishes more than one entry point, and the split is informative.

The main entry point is the core library, with a minified CommonJS build, a minified ES module build and the type definitions. There is a separate ./serialization export with its own type definitions and both module formats, and a separate ./legacy export with the same three. The typesVersions map repeats the subpath types for resolvers that need it.

The existence of a legacy entry point says something about the project's history, and the documentation section list supports it. There is an Introduction page, a Serialization page, a Multithreading page and an API page.

Serialization being a separate import rather than part of the core is a sensible choice for a zero dependency library with an about 5kb minzipped core, because the save and load machinery is not something every application needs and pulling it in would grow the bundle for no reason. The same logic would apply to the legacy path, which is presumably kept for applications that have not migrated yet.

The build scripts show how the package is produced. The build runs the core, serialization and legacy builds and then the types, the core build produces an ES module and a CommonJS build, each in minified and non minified variants, and esbuild does the bundling with a sourcemap. The types are built with the TypeScript compiler, and the repository has separate tsconfig files for the core, the serialization and the tests, which is the usual way to keep a subpath's compilation separate from the rest.

The tests run with bun test, and the API documentation is generated from the source with a documentation build over the core source files, writing markdown to docs/API.md. Generating the API reference from the source rather than writing it by hand means the reference cannot drift, and the fact that the reference is a markdown file in the repository means it is readable on GitHub without running anything.

The repository is small and flat: src/, test/, docs/, examples/, dist/, a bun lockfile, and an examples directory with a networking example.

## Thread-friendly, which is a constraint as much as a feature

The feature table lists thread-friendly as one of the listed properties, and there is a dedicated documentation page on multithreading. The word to pay attention to is friendly, because the library is not going to do the threading for you.

The example code is single threaded, and it is written in the way that survives being moved to a worker: the world is a plain object of plain data, the components are plain arrays and typed arrays, and the systems are pure functions over that data. There is no class hierarchy to inherit, no hidden state in a component instance, and nothing that cannot be structured cloned or transferred to a worker.

That is what makes the design thread-friendly, and it is also why the data-oriented choices matter here rather than only for cache performance. A component stored as an object per entity is awkward to move between threads; a component stored as parallel typed arrays is a buffer you can transfer.

So the library leaves the concurrency model to you. The example loop, whether it is the setInterval used for the Node environment or the requestAnimationFrame used in the browser, is the only place where the timing is decided, and a threaded version would mean running that same update over a different set of worlds, or over shards of one.

What you do not get is help. There is no scheduler that runs systems in parallel, no job system, no thread pool. The documentation page is where the intended pattern is described, and it is the page to read before assuming that a bitECS world can simply be moved into a worker.

## Bundle size, dependencies and the build system

Two numbers in the feature table define the intended deployment environment: around 5kb minzipped and zero dependencies.

Zero dependencies is worth more than it sounds for a library that is meant to sit in a game bundle, because every dependency is bytes and every dependency is a version you track. It also means the library is not going to break when a transitive dependency changes its major version, which for an engine primitive is a desirable property.

The 5kb figure is the minzipped size of the core, and the build produces both minified and non minified variants of both module formats, so an application that wants to inspect the code in a debugger can use the non minified build. The sourcemaps are produced as well.

The build tooling is Bun for the test runner and for invoking the build scripts, esbuild for the actual bundling, and the TypeScript compiler for the type definitions. The documentation is generated with a documentation tool invoked through bunx, which is why the API reference is a checked-in markdown file.

The repository also carries a dist/ directory in the listing, which for a package published from source usually means the built output is committed or at least present in the working tree, and the presence of a.npmignore alongside it is the sign that the published tarball is trimmed.

One last practical note for anyone evaluating a data-oriented ECS: the library is young in version terms, at 0.4.0, with no GitHub releases to browse and a release history you can only infer from the npm versions. The API surface is small enough that reading the source is a reasonable way to judge whether it fits, and the repository is small enough to read in an afternoon.

## Conclusion

bitECS suits a browser or Node game developer who wants the data-oriented approach to entity state without adopting a framework's opinion about how to organise it, and it suits a team migrating from an object oriented ECS that felt too heavy. It is the wrong choice if you want a managed engine with a scheduler, a scene graph and asset loading, because the library supplies the entity, component, query and system primitives and little else, and a poor match if you are not willing to think about memory layout, since the library will happily let you put your hot data in an array of objects. Before adopting, decide the storage layout of your hot components yourself, and read the multithreading and serialization pages, since those are the two places where the flexibility has consequences.

## FAQ

### How do I install bitECS?

Run npm i bitecs. The package has zero dependencies and an about 5kb minzipped core, with the main entry point available as a minified ES module and a minified CommonJS build, plus separate ./serialization and ./legacy entry points.

### How are components declared in bitECS?

They can be any shape you want, declared when the world is created. The example shows a structure of arrays with plain arrays and a Float32Array for velocity, and an array of structures holding an object per entity, and both access patterns are supported.

### How do systems and the frame loop work in bitECS?

Systems are plain functions that take the world and update entities, and a query selects the entities that hold a given set of components. The example drives its update function from a setInterval in a Node environment and from requestAnimationFrame in the browser, so the frame loop is yours to write.

### What licence is bitECS released under?

MPL-2.0, a file level copyleft, with the author listed as Nathaniel Martin. The current published version is 0.4.0, the repository is not archived, and the last push was on 2026-08-24 with no GitHub releases published.

## Sources

- [Issues](https://github.com/NateTheGreatt/bitECS/issues)
- [License: MPL-2.0](https://github.com/NateTheGreatt/bitECS/blob/main/LICENSE)
- [NateTheGreatt/bitECS on GitHub](https://github.com/NateTheGreatt/bitECS)
- [Project website](https://bitecs.dev)
- [README](https://github.com/NateTheGreatt/bitECS/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/natethegreatt-bitecs
