Measured on Bun 1.4.1, Apple M3 Pro, three fresh processes each, RSS after a forced GC:
| Imported |
RSS |
| nothing |
11.8 MiB |
@stacksjs/bun-router (its barrel) |
20.7 MiB |
@stacksjs/router |
25.9 MiB |
So the published router runtime's barrel costs 8.9 MiB, and the Stacks layer on top of it another 5.2 MiB, before a single route is registered. The barrel is 529 KB of JavaScript across dist/index.js and three chunks, and it carries the whole surface: websockets, sessions, caching, container, observability, testing, docs, CLI.
For context, bench/memory at its 25,000 req/s profile (20s load, 40s idle, settled median):
| Target |
Settled idle RSS |
Bun.serve baseline |
14.3 MiB |
| Hono |
46.7 MiB |
| Elysia |
50.5 MiB |
| Stacks (stock defaults) |
52.4 MiB |
| Fastify |
87.9 MiB |
| Express |
95.0 MiB |
Stacks loses that row to Hono by 5.7 MiB and to Elysia by 1.9 MiB - both smaller than the 8.9 MiB the barrel costs on its own. A server that loaded only the routing it uses would take the row outright.
The barrel is loaded because @stacksjs/router's entry point does export * from '@stacksjs/bun-router', deliberately, so userland gets the whole surface from one import. That is a real API decision and not something to quietly reverse - an app that imports @stacksjs/router today can reach any of it.
What would need to happen
@stacksjs/bun-router (separate repository) exposes the serve/dispatch path through an entry point that does not pull websockets, sessions, cache, container, testing, docs and the CLI in with it. Its exports map already has subpaths, so this is about what dist/index.js re-exports eagerly rather than about adding a map.
- Stacks then gets a narrow entry - the equivalent of "give me a server and routes" - that an API-only or serverless deployment can import instead of the full barrel. The existing barrel keeps working unchanged.
bench/memory gains a row for it, so the saving is measured rather than asserted.
Filed with numbers rather than fixed here because it spans two repositories and changes a public entry point.
Measured on Bun 1.4.1, Apple M3 Pro, three fresh processes each, RSS after a forced GC:
@stacksjs/bun-router(its barrel)@stacksjs/routerSo the published router runtime's barrel costs 8.9 MiB, and the Stacks layer on top of it another 5.2 MiB, before a single route is registered. The barrel is 529 KB of JavaScript across
dist/index.jsand three chunks, and it carries the whole surface: websockets, sessions, caching, container, observability, testing, docs, CLI.For context,
bench/memoryat its 25,000 req/s profile (20s load, 40s idle, settled median):Bun.servebaselineStacks loses that row to Hono by 5.7 MiB and to Elysia by 1.9 MiB - both smaller than the 8.9 MiB the barrel costs on its own. A server that loaded only the routing it uses would take the row outright.
The barrel is loaded because
@stacksjs/router's entry point doesexport * from '@stacksjs/bun-router', deliberately, so userland gets the whole surface from one import. That is a real API decision and not something to quietly reverse - an app that imports@stacksjs/routertoday can reach any of it.What would need to happen
@stacksjs/bun-router(separate repository) exposes the serve/dispatch path through an entry point that does not pull websockets, sessions, cache, container, testing, docs and the CLI in with it. Itsexportsmap already has subpaths, so this is about whatdist/index.jsre-exports eagerly rather than about adding a map.bench/memorygains a row for it, so the saving is measured rather than asserted.Filed with numbers rather than fixed here because it spans two repositories and changes a public entry point.