How Solid + Vite + Hono cut Trinity's cold launch 37%

Lorenzo Wynberg10 min readEngineering
Trinity's stack before and after — Next.js + React + standalone server on the left, Solid + Vite + Hono on the right, with bundle, launch, and memory deltas underneath

Trinity's desktop app is now built on Solid + Vite + Hono. Same features, same data, same shortcuts. The app launches in roughly half the time, ships a third less code, and holds ~70 MB less memory while running.

The auto-updater installs the new build in place. Your projects, knowledge base, secrets, and sign-in state all stay where they are — no migration, no manual steps. If you've already installed Trinity, you'll get it on the next update check. The full release notes are in the 0.2.0-beta changelog.

Here's why we tore Next.js out, what replaced it, and what the measurements actually look like.

Why Next.js had to go

Trinity launched on Next.js for a reason. In early 2026, the desktop owned its own data — projects, PRDs, stories, jobs, knowledge bases, releases all lived in a local SQLite that synced to a per-user Turso replica. Next gave us route handlers for every CRUD endpoint, server-side rendering for a fast first paint, and the App Router for layouts. It fit what the app was.

Then the data moved.

Over the last few months we shifted the data plane to trinityailabs.com: 99 typed /api/* routes for projects, stories, knowledge, releases, secrets, activity, recaps — the lot. The desktop became a thin client that fetches over HTTP and renders. The local SQLite shrank to about ten machine-only tables (worktrees, workers, coordinator state, chat sessions, tasks, device config).

That left Next.js doing one job: serving the UI.

The cost we were paying

The framework was still doing that job well. It just cost too much to do it.

  • SSR cold-compile on every fresh boot. Every cold start paid for route-handler compilation before the first byte went out.
  • Turbopack module mangling we had to patch around in the bundler config.
  • The .next/standalone packaging dance for the Tauri sidecar — a hoisted node_modules tree, a parallel workspace dir, and a build script that we kept feeding new edge cases.
  • A heavyweight client runtime that took ~1.4 seconds to hydrate React after the server was already responsive.

For an app with no SSR-shaped requirements left, that's a tax with no benefit. The cost-benefit had inverted. We weren't getting paid back for the framework's complexity anymore.

Why Solid + Vite + Hono

Three pieces, each picked because it's the smallest version of what we needed.

BeforeNext.js + React
TauriWKWebView shell
Next.js 16UI + SSR + 99 API routes
React 19Hydration + render
.next/standaloneSidecar packaging
Embedded NodeRuntime
Rewrote
AfterSolid + Vite + Hono
TauriWKWebView shell
SolidFine-grained reactivity
ViteStatic SPA build
Hono99 routes mounted at /api/*
Embedded NodeRuntime
Bundle on disk−33%
Cold launch−37%
Idle memory−19%
  • Solid for the UI. Fine-grained reactivity, no virtual DOM, no hydration pause. When state changes, only the affected DOM updates. Components compile to direct DOM operations.
  • Vite for the build. Static SPA output, instant HMR in dev, clean ESM bundles in prod.
  • Hono for the sidecar server. A 5 MB Node bundle that mounts all 99 of our route files under /api/* and serves the static SPA via serveStatic.

Why Hono made the route port trivial

Hono uses the same Request / Response types Next was passing around. Every existing route handler ported with minimal changes — same paths, same payloads, same auth middleware, same ExecCtx plumbing. The 99 route files moved from src/app/api/.../route.ts to server/routes/.../*.ts and kept working.

The Tauri shell is unchanged. It still picks a free port, spawns the Node sidecar, and navigates the WKWebView to http://127.0.0.1:<port> once the HTTP probe succeeds. The bundle layout flattened — instead of Next's standalone/ tree with hoisted node_modules, the new sidecar is one src-tauri/server/ with server.js, the SPA's dist/, the embedded Node binary, and a small node_modules/ carrying just the native externals (@tursodatabase/sync, node-pty, playwright-core).

Why this shape suits an agent IDE

The numbers are the easy part. The harder thing to capture is what Solid's reactivity model does to a UI that never stops moving.

Trinity's main view is a coordination dashboard. At any given moment there's a planner streaming tokens into a PRD draft, three or four worker panels emitting live log lines, story cards re-ordering as their statuses change, audit gates flipping green or red, file diffs scrolling in. None of these surfaces are static — they update by the second.

In React, every one of those updates re-runs the component that owns it, then the reconciler compares the resulting virtual tree against the previous one before deciding what DOM to touch. With careful memo and useMemo boundaries you can constrain the blast radius, but the baseline cost is proportional to the size of the rendered subtree. For Trinity's busiest screens that's expensive: ten worker rows × thirty visible log lines × one update per token = a lot of diffing nobody asked for.

Solid moves the work in the other direction. State lives in signals. Each signal knows exactly which DOM nodes depend on its value, and a signal change updates only those nodes — no component re-run, no tree diff. The cost of a single update is constant in the size of the dependent set, not the tree. For a UI like Trinity's, that's a structural change, not a constant-factor speedup.

A few related effects we noticed:

  • Bundle and runtime. Solid's runtime is ~7 KB minified vs React + ReactDOM at ~45 KB. In a Tauri webview that's open all day, every kilobyte of runtime is RAM and parser time we don't have to spend.
  • No hydration boundary. React mounts an interactive copy on top of pre-rendered HTML on first paint. Solid mounts directly. For a desktop SPA the hydration step bought us nothing and removed a whole class of mismatch bugs by going away.
  • Long-session memory profile. Closures and hooks state in React allocate heavily on every re-render; the GC handles it but the heap drifts upward over a day-long session. Solid's signal graph is comparatively stable — fewer allocations per UI update, less churn for the GC to chase.
  • No Server Components tax. Trinity's renderer is a webview. There's no SSR boundary to rationalize, no 'use client' / 'use server' axis to keep in your head, no bundle-splitting story to reason about. The whole "where does this run" question disappears and the mental model gets simpler.
  • Vite HMR in dev. Cold dev start dropped from ~9 s to ~600 ms; route-level edits hot-reload instantly instead of recompiling a route handler. That compounds over a day of frontend work in a way the production numbers don't capture.

None of this is a takedown of React. React is what we built Trinity on and what most of the team's frontend instincts default to. The point is narrower: for the specific shape Trinity's UI has — long-running, high-frequency updates, no SSR-shaped requirements, a fixed Tauri host — Solid's structural properties line up with our load profile in a way that React's were starting to fight against.

What we kept on React (and why)

Two narrow surfaces still run on React 19, mounted as islands inside the Solid host:

  • /graph — the dependency graph view, built on @xyflow/react + react-pacer. xyflow has no Solid equivalent we'd ship without writing it ourselves.
  • /code — the read-only code viewer (CodeMirror 6 + react-arborist file tree + cmdk command palette). Same story.

We considered porting both. The honest answer is the islands are small (~3 MB combined including their React runtime), self-contained, and the React ecosystem is the better fit for where xyflow and react-arborist live. Wholesale rewrites for ideology cost more than they earn. The Vite config marks the island file globs explicitly; everything else compiles through vite-plugin-solid.

The numbers

Both apps installed side-by-side on the same Mac, both Gatekeeper-friendly (Next.js build via CI notarization, Solid build trusted manually). Buffer cache flushed (sudo purge) and WebKit cache cleared (rm -rf ~/Library/WebKit/<bundle-id>/) before each iteration — true cold start, three iterations averaged.

MetricNext.js buildSolid buildDelta
Bundle on disk388 MB262 MB−33%
Cold launch (3-iter avg)3080 ms1931 ms−37% (1.15 s)
Cold spawn → server listening1682 ms1690 mstied (Node boot + initDb dominates both)
Cold listen → UI ready1398 ms241 ms−83%
RSS at idle (3-iter avg)~348 MB~281 MB−19% (67 MB)
/api/health first response~260 ms~1 ms−99.6%

The cold-launch story splits cleanly into two phases — and only one of them moved.

Cold launch — where the 1.15s came from

Next.js buildBefore
3080 ms
Solid buildAfter
1931 ms
Spawn → server listening (kernel + Node + initDb)Next: SSR compile + hydrateSolid: serve static + render

Spawn → server listening is identical on both stacks. That phase is dominated by the kernel verifying signed Mach-Os, Node starting up, and initDb() migrating the local SQLite. None of that got faster. We didn't pretend it would.

Server listening → UI ready is where the win is. On Next, that phase is route-handler compilation + SSR + sending HTML + hydrating React — about 1.4 seconds of work after the server is already responsive. On Solid, the Hono server reads dist/index.html from disk, the browser parses ~1 MB of JS, and Solid hydrates from local state. 241 ms.

The same shape shows up in /api/health. Next was lazy-compiling the route handler on first hit and paying ~250 ms for it; every fresh process pays the tax again. Hono's routes are bound at server start — sub-millisecond on the first hit and every hit after.

After the first launch, with LaunchServices' Gatekeeper verdict cached and the OS file cache warm, the gap shrinks but stays material: warm cold-start is ~1.0 s on Solid vs ~2.0 s on Next. That's the experience you'll feel day-to-day.

What didn't change

Every URL still works. Every shortcut still works. The data plane on trinityailabs.com is unchanged — this rewrite touched only how the desktop renders the UI it was already rendering.

  • Local SQLite at ~/.trinity/trinity-{slot}.db keeps the same schema and the same migrations.
  • Per-account auth state at ~/.trinity/accounts-{slot}/<account-id>/ is untouched.
  • All 99 /api/* route files moved from src/app/api/.../route.ts to server/routes/.../*.ts — same path, same payload, same auth middleware.
  • Tauri identifier stays com.trinity-ai.desktop — signing keys, updater endpoints, and your installed Trinity.app identity all carry over. The auto-update from 0.1.0-beta.1 → 0.2.0-beta installs in place.
  • Your app settings and secrets carry over with no changes.

If you'd manually pinned anything (workspace paths, env vars, custom scripts hitting localhost), nothing breaks.

What we kept honest

warning

Two caveats worth naming up front, so the numbers above don't oversell.

  • spawn → listen didn't move because the costly parts (kernel signature checks, Node startup, initDb() migrations) are the same on either stack. We're not claiming a magic Node-startup-was-secretly-slow story — Node starts in ~1 second whether the file it loads is a Hono bundle or a Next standalone.
  • The React islands carry their own React 19 + react-dom runtime. That's about 130 KB gzipped sitting inside two specific routes. The rest of the app is React-free.

What this unlocks

The rewrite was the prerequisite, not the product. The desktop now gets out of the way faster than the agents do — and that's the point.

When the UI ships in 241 ms instead of 1.4 seconds, we can iterate on the multi-agent execution pipeline without the framework taxing every demo. When idle memory is 70 MB lower, there's more headroom for the agents themselves on machines that are already running a coordinator, a couple of workers, and Docker. When the bundle is 33% smaller, shipping richer surfaces — better visualizations of the knowledge vault, more aggressive in-app planning UIs — doesn't blow up the install footprint.

That's the work the next few releases are about. The execution pipeline. The planning loop. The knowledge that compounds across runs. The same things Trinity has always existed for — just on a chassis that doesn't fight them.

If you've installed Trinity, the auto-updater will pull this in shortly. If you haven't yet — grab it here.