Benchmarks
Fullstack comparison: method and data
One issue tracker, built with Lumiose and seven other stacks, measured end to end: dev server, build, page load and server throughput. This page has the rules, the code of each app, every committed results file and the commands to run it yourself.
The app
Every stack implements the same issue tracker ("Tracker"): a shared layout (header with the current user, a sidebar of 12 projects with their open-issue counts, a footer) and these routes:
| Route | Content | Interactive part |
|---|---|---|
/ |
Dashboard: status and priority counts, a per-project table, 10 recent issues | None |
/projects/:slug |
A GET filter form, 50 issues per page, pagination, the total count | Bulk select: row checkboxes, "N selected", "Close selected" posts an action |
/issues/:id |
The issue, a status form (action), and the comments streamed behind a fallback | A comment composer with a live character count |
/issues/new |
A form validated on the server, with field errors; success redirects to the issue | A label picker |
/search?q= |
Search results | A search box with debounced suggestions from /api/search |
/login |
A username form that sets the session cookie | None |
/api/issues/:id, /api/search?q= |
JSON endpoints | None |
The data is one SQLite database per app (node:sqlite, a deterministic seed: 200 users, 12
projects, 6,000 issues, 30,000 comments), read through one shared package with the same queries
for every app. Mutating actions need a session and redirect to /login without one.
Fairness rules
- Same app, same output. Same routes, data, HTML structure, class names and behaviour. One Playwright conformance suite runs against every app in production mode, and an app is only measured when it passes.
- Same CSS and data layer. Every app imports one plain stylesheet and calls the same data package. No ORM, no CSS Modules or utility CSS, no framework data cache.
- Idiomatic code. Each app uses its framework's recommended primitives (the implementations below), including how it streams the slow comments.
- Dynamic rendering everywhere. Every page renders on each request: no prerendering and no page or data caching.
- Production servers as shipped, with HTTP compression off in every app; payload sizes are
computed offline with gzip (level 6) and brotli (quality 11). The harness checks, on each
server's first start, that it runs with
NODE_ENV=productionand does not compress. - Latest stable versions, pinned exactly; each results file records the resolved versions.
- Same machine, sequential runs, nothing else heavy running; each file records the machine. Server load tests run all apps at once and interleave their rounds, rotating the order each round, after a cool-down and an idle check, so a load spike cannot land on one framework.
What is measured
Every number is a median over several iterations, with the minimum and maximum kept (the viewer draws them as whiskers). Each metric says where it was measured:
- Development (each app's dev server): cold and warm start, first and warm page loads, and the time from editing a shared component to the browser showing it.
- Build (the production build and the sources): build time, output size on disk, type check
time, lines of source, and the packages the production server loads at run time (traced, not
read from
package.json). - Production (the built app on its production server): the JavaScript and CSS each first page load downloads, page load timings in Chromium with 4× CPU throttling (TTFB, FCP, LCP, total blocking time, time until the interactive parts are ready), and server throughput and latency with autocannon (warm-up, then 3 × 10 s with 50 connections per route), cold start and memory.
The results viewer shows every metric of a run as a table and a bar chart, and compares two runs.
Reproduce a run
You need Node.js 24 or later, pnpm, Rust (for the Lumiose compiler) and a quiet machine.
git clone https://github.com/itsjavi/lumiose.git && cd lumiose
pnpm install
pnpm build:compiler
pnpm --filter @bench/harness exec playwright install chromium
# Check that every app produces the same output (the conformance suite).
pnpm --filter @bench/harness conformance
# A full run writes benchmarks/fullstack/results/<date>T<time>-<host>.json.
pnpm --filter @bench/harness bench
# A smoke run: one iteration, some apps, some metric groups.
pnpm --filter @bench/harness bench --quick --only lumiose,next --metrics build,load
# Browse the results, yours included, in the viewer.
pnpm --filter @bench/harness run view
bench also takes --network slow4g (a throttled network for page loads), --comments-delay <ms>
(slow comments, to see streaming) and --iterations dev=5,build=3,load=5,server=3. Every run gets
a file of its own; a --quick or --only run never replaces another one.
The implementations
Versions and production servers as the newest run recorded them. Shared code lives in packages (data, stylesheet, React components), and the runner and the conformance suite in harness.
| Stack | How it is built | Production server |
|---|---|---|
| Lumioselumiose c69f45a (commit) | Routes in server mode, async components with <Suspense> for the streamed comments, form actions, and islands (client:*) for the four interactive parts. | NODE_ENV=production node server.ts |
| React Routerreact-router 8.4.0 · react 19.3.0 | Framework mode: loaders and actions, <Form> and useFetcher, a loader promise with <Await> inside <Suspense>. The whole page hydrates. | NODE_ENV=production node server.ts |
| Next.jsnext 16.4.0 · react 19.3.0 | App Router: server components, server actions, "use client" components for the interactive parts, an async server component inside <Suspense>. | next start --port ${PORT:-4103} |
| SolidStart@solidjs/start 2.0.6 · solid-js 1.9.17 | query and createAsync server functions, actions with useSubmission, a non-deferred createAsync inside <Suspense>. The whole page hydrates. | NODE_ENV=production node .output/server/index.mjs |
| SvelteKit@sveltejs/kit 3.0.1 · svelte 5.57.2 | +page.server.ts loads and form actions with use:enhance, an un-awaited load promise rendered with {#await} (streamed as data). The whole page hydrates. | NODE_ENV=production node build |
| Nuxtnuxt 4.6.1 · vue 3.5.43 | useFetch from Nitro server routes, $fetch mutations with refresh, and the comments loaded in the browser, since Vue's server renderer does not stream a fallback. The whole page hydrates. | NODE_ENV=production node .output/server/index.mjs |
| Remix 3remix 3.0.0 | Route maps and controllers, native forms posting to actions, clientEntry components for the interactive parts, a <Frame> with a fallback for the comments. No build step. | NODE_ENV=production node --import remix/node-tsx server.ts |
| Astroastro 7.3.8 · preact 10.29.8 | On-demand rendering, .astro pages, forms posting to their own page, Preact islands (client:load), and a server island (server:defer) for the comments. | NODE_ENV=production HOST=127.0.0.1 PORT=${PORT:-4108} node dist/server/entry.mjs |
Raw results
Every committed run, newest first, as the JSON files the viewer reads (index.json lists them). A full run measures every metric group it names; a quick run is one iteration, for smoke tests. The site's charts use the newest full run that measured each metric.
| Run | Machine | Metric groups | Apps | File |
|---|---|---|---|---|
| 2026-10-12 16:21 UTC | Apple M1 Max (10 cores), macOS 27.0.1, Node 26.8.1 | server | 8 | JSON · GitHub |
| 2026-10-12 11:45 UTC | Apple M1 Max (10 cores), macOS 27.0.1, Node 26.8.1 | dev, build, load, server, other | 8 | JSON · GitHub |
| 2026-10-09 08:58 UTC | Apple M1 Max (10 cores), macOS 27.0, Node 26.8.1 | server | 3 | JSON · GitHub |
| 2026-10-09 08:48 UTC | Apple M1 Max (10 cores), macOS 27.0, Node 26.8.1 | dev, build, load, server, other | 3 | JSON · GitHub |
| 2026-10-09 05:23 UTCquick | Apple M1 Max (10 cores), macOS 27.0, Node 26.8.1 | dev, build, load, server, other | 3 | JSON · GitHub |
| 2026-10-09 04:53 UTCquick | Apple M1 Max (10 cores), macOS 27.0, Node 26.8.1 | dev, build, load, server, other | 3 | JSON · GitHub |