{"id":50609447,"url":"https://github.com/andymai/ecsia","last_synced_at":"2026-06-10T03:01:25.582Z","repository":{"id":362684986,"uuid":"1260140502","full_name":"andymai/ecsia","owner":"andymai","description":"Fast, type-safe entity component system for TypeScript with automatic multithreading","archived":false,"fork":false,"pushed_at":"2026-06-06T01:03:19.000Z","size":1316,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-06T02:23:50.284Z","etag":null,"topics":["data-oriented","ecs","entity-component-system","gamedev","multithreading","typescript"],"latest_commit_sha":null,"homepage":"https://andymai.github.io/ecsia/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andymai.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-05T07:37:57.000Z","updated_at":"2026-06-06T01:03:08.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/andymai/ecsia","commit_stats":null,"previous_names":["andymai/ecsia"],"tags_count":183,"template":false,"template_full_name":null,"purl":"pkg:github/andymai/ecsia","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andymai%2Fecsia","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andymai%2Fecsia/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andymai%2Fecsia/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andymai%2Fecsia/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andymai","download_url":"https://codeload.github.com/andymai/ecsia/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andymai%2Fecsia/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34134633,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["data-oriented","ecs","entity-component-system","gamedev","multithreading","typescript"],"created_at":"2026-06-06T02:01:28.839Z","updated_at":"2026-06-10T03:01:25.576Z","avatar_url":"https://github.com/andymai.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# ecsia\n\nBuild simulations out of plain data, and let ecsia run them across threads for you.\n\n[![CI](https://github.com/andymai/ecsia/actions/workflows/ci.yml/badge.svg)](https://github.com/andymai/ecsia/actions/workflows/ci.yml)\n[![License](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)\n\n**[Getting Started](https://andymai.github.io/ecsia/guide/getting-started)** · **[Core Concepts](https://andymai.github.io/ecsia/guide/core-concepts)** · **[Multithreading](https://andymai.github.io/ecsia/guide/parallelism)** · **[Performance](https://andymai.github.io/ecsia/guide/performance)**\n\n\u003c/div\u003e\n\necsia is an entity component system (ECS) for TypeScript. If you haven't met the\npattern before, it's a way of organizing a simulation — a game, a physics sandbox, an\nagent model — around three simple ideas:\n\n- An **entity** is just an id. A thing in your world, with no data of its own.\n- A **component** is a typed piece of data you attach to an entity — a position, a\n  velocity, a health value.\n- A **system** is a function that runs every frame over all entities that have a\n  particular set of components.\n\nA \"bird\" isn't a class — it's whatever entity happens to have a position and a\nvelocity, and movement is a system that adds one to the other sixty times a second.\nWant a bird with health? Attach a health component. Composition replaces class\nhierarchies.\n\nThe payoff is speed and safety at the same time. Under the hood, each component field\nlives in its own contiguous typed array, so looping over 50,000 entities walks\nstraight through memory the way CPUs like. On top of that sits a fully typed API —\n`e.position.x` is a `number`, not a cast.\n\n```ts\nimport {\n  createWorld, defineComponent, defineSystem, createScheduler, read, write,\n} from '@ecsia/kit'\n\nconst Position = defineComponent({ x: 'f32', y: 'f32' }, { name: 'position' })\nconst Velocity = defineComponent({ dx: 'f32', dy: 'f32' }, { name: 'velocity' })\n\nconst world = createWorld({ components: [Position, Velocity], maxEntities: 1 \u003c\u003c 16 })\n\nconst e = world.spawnWith(Position, Velocity)\nworld.entity(e).write(Velocity).dx = 5\n\nconst dt = 1 / 60\nconst Movement = defineSystem({\n  name: 'Movement',\n  read: [Velocity],   // this system only reads velocities…\n  write: [Position],  // …and only writes positions\n  run({ query }) {\n    for (const e of query(read(Velocity), write(Position))) {\n      e.position.x += e.velocity.dx * dt\n      e.position.y += e.velocity.dy * dt\n    }\n  },\n})\n\nconst scheduler = createScheduler(world, [Movement])\nscheduler.update(dt) // run one frame\n```\n\nThose `read` and `write` declarations aren't just documentation. From them, the\nscheduler works out which systems can never interfere with each other — and runs\nthose at the same time on worker threads. Going parallel is one flag, with no changes\nto any system, query, or accessor code:\n\n```ts\nimport { createWorld } from '@ecsia/kit'\n\nconst world = createWorld({ components: [/* ... */], threaded: true })\n```\n\n## Guarantees\n\n- **Threads never change your results.** A threaded run produces exactly the same\n  entities, the same component values, and the same change events as running\n  everything on one thread — byte for byte. This isn't a promise, it's a tested\n  property: the test suite runs the same simulations both ways, against a real worker\n  pool, and checks the outputs are identical.\n- **Stale references throw.** `world.entity()` hands out a reusable reference object\n  rather than allocating a new one each call. If you hold onto one after it has been\n  re-pointed at another entity, you get an error — never another entity's data.\n- **No silent slowdowns.** Threading needs shared memory (`SharedArrayBuffer`; in\n  browsers that requires cross-origin isolation, a server-side opt-in). Where it's\n  unavailable, ecsia logs a warning and runs on one thread. Work is never silently\n  dropped.\n\n## What ecsia leaves to you\n\nRendering, physics, input, audio, networking — ecsia is the data and scheduling layer\nunderneath a simulation, not an engine. [`@ecsia/three`](./packages/three) keeps\nthree.js objects in sync with your components, and\n[`@ecsia/serialization`](./packages/serialization) turns world state into payloads you\ncould save or send over a network — but drawing frames and shipping packets is your\ncode.\n\nTwo constraints worth knowing up front: every component's fields are declared ahead\nof time (both the memory layout and the type inference depend on it), and the whole\nthing is ESM-only.\n\n## Status\n\n0.1.0 — feature-complete, API-frozen, not yet on npm. Young; expect rough edges.\nRuns on Node 22+, Bun, Deno, and modern browsers.\n\n## Install\n\n```sh\npnpm add @ecsia/kit\n```\n\n\u003e **Not yet published.** `@ecsia/kit` is staged but not on npm yet — the command above\n\u003e will not resolve until first publish. For now, consume it from the workspace.\n\nThe umbrella package is the intended entry point. The layers underneath\n(`@ecsia/core`, `@ecsia/scheduler`, …) publish separately for anyone who wants to\ncompose them by hand.\n\n## Documentation\n\n- **[Getting Started](https://andymai.github.io/ecsia/guide/getting-started)** — install, first world, first system\n- **[Core Concepts](https://andymai.github.io/ecsia/guide/core-concepts)** — entities, components, queries, and how ecsia stores them\n- **[Multithreading](https://andymai.github.io/ecsia/guide/parallelism)** — how `threaded: true` works, what runs where, and why results stay identical\n- **[Linking entities](https://andymai.github.io/ecsia/guide/relations)** — parent/child hierarchies and other entity-to-entity links\n- **[Reacting to changes](https://andymai.github.io/ecsia/guide/reactivity)** — run code when components are added, removed, or modified\n- **[Saving and syncing](https://andymai.github.io/ecsia/guide/serialization)** — snapshots, change payloads, worker handoff\n- **[three.js bridge](https://andymai.github.io/ecsia/guide/three-bridge)** — keeping three.js objects in sync with your data\n- **[Devtools](https://andymai.github.io/ecsia/guide/devtools)** — inspect a world and see why the scheduler made its choices\n- **[Performance](https://andymai.github.io/ecsia/guide/performance)** — measured benchmarks, methodology, reproduce instructions\n\nThe full site — these guides plus a generated API reference — lives at\n**[andymai.github.io/ecsia](https://andymai.github.io/ecsia/)** and redeploys on every\npush to main. The sources are in [`website/`](./website) (`pnpm docs:dev` for a local\npreview).\n\n## Packages\n\n| Package | Role |\n|---|---|\n| `ecsia` | the batteries-included umbrella — start here |\n| `@ecsia/core` | component storage, typed accessors, queries, change tracking |\n| `@ecsia/schema` | component field types and query type inference |\n| `@ecsia/scheduler` | works out which systems can run together, and runs them across threads |\n| `@ecsia/relations` | entity-to-entity links with fast queries and automatic cleanup |\n| `@ecsia/serialization` | snapshots, change payloads, worker bootstrap |\n| `@ecsia/three` | three.js bindings (opt-in, not in the umbrella) |\n| `@ecsia/devtools` | world inspector and schedule explainer (opt-in) |\n\n`@ecsia/core` is a complete single-threaded ECS on its own. Scheduling, relations,\nand serialization plug into it without core knowing about them; nothing imports\nupward. All packages are `sideEffects: false`, so bundlers drop whatever you don't\nuse.\n\n## Benchmarks\n\nReal measured numbers, regenerated by `pnpm bench:report` (one machine, one moment —\ntreat the shapes as durable, the milliseconds as a snapshot; AMD Ryzen 9 7950X3D,\nNode v24.11.0).\n\nSingle-thread iteration — the classic ECS workload of adding each entity's velocity\nto its position, over 50,000 entities, against bitECS 0.4.0 and miniplex 2.0.0.\nLower is faster (nanoseconds per entity):\n\n| loop | ns per entity |\n| --- | ---: |\n| ecsia `bindColumns` | 0.97 |\n| bitECS | 1.35 |\n| ecsia `eachChunk` | 1.47 |\n| ecsia `.each` | 10.14 |\n| miniplex | 12.15 |\n\n`.each` is the ergonomic accessor path from the example above; `eachChunk` loops over\nthe raw storage arrays directly; `bindColumns` goes one step further and compiles a\nspecialized loop per archetype — which is what lets it beat bitECS, and it holds that\nedge as the world grows (no pre-sizing required; it falls back to a plain loop where a\nstrict CSP or sandbox forbids dynamic compilation).\n\nYou don't have to choose between the readable `.each` body and that speed:\n[`query.compile`](https://github.com/andymai/ecsia/blob/main/website/guide/performance.md#compile-the-ergonomic-path-compile)\ntakes the same `e.position.x += …` callback, rewrites it into the `bindColumns`-shape loop, and lands\nnear `eachChunk` — roughly 6× faster than the plain `.each` it's written like — while still feeding\n`.changed()`/observers. It's a pure speedup that falls back to the normal loop for anything it can't\ncompile.\n\nWorker-thread speedup on a compute-heavy simulation (8,192 entities, 512 physics\nsteps per frame, 60 frames), with every threaded run byte-identical to the\nsingle-threaded result:\n\n| workers | speedup |\n| ---: | ---: |\n| 1 | 0.99x |\n| 2 | 1.90x |\n| 4 | 3.62x |\n| 8 | 6.38x |\n\nMethodology and full tables on the [performance page](https://andymai.github.io/ecsia/guide/performance).\n\n## Development\n\n```sh\npnpm install\npnpm build              # tsc -b across all packages\npnpm test               # vitest: unit + property + worker + type-level\npnpm typecheck:extras   # type-check examples/ and bench/\npnpm bench:macro        # cross-library macro-benchmarks\n```\n\nRunnable examples in [`examples/`](./examples): a flock of birds, a parent/child\nscene hierarchy, a worker-parallel simulation, and a damage-over-time effect with\nautomatic cleanup.\n\n## License\n\n[MIT](./LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandymai%2Fecsia","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandymai%2Fecsia","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandymai%2Fecsia/lists"}