Flagship

The web framework with two modes.

Static routes ship zero JavaScript.

App routes ship the full interactive experience.

Same project. Same router. You choose.

$npm create neutron@latest
routes/about.tsx
export const config = { mode: "static" };

export default function About() {
  return (
    <main>
      <h1>About Us</h1>
      <p>We build tools for developers.</p>
    </main>
  );
}
Output: 1.2 KB HTML. Zero JavaScript. 100 Lighthouse.
Static route
routes/app/dashboard.tsx
export const config = { mode: "app" };

export async function loader({ context }: LoaderArgs) {
  return {
    projects: await context.db.getProjects(),
  };
}

export default function Dashboard() {
  const { projects } = useLoaderData<typeof loader>();
  return (
    <main>
      <h1>Dashboard</h1>
      <ul>
        {projects.map(p => (
          <li key={p.id}>{p.name}</li>
        ))}
      </ul>
    </main>
  );
}
Output: SSR + 3 KB Preact hydration. Typed. Interactive.
App route
File-based nested routing
Parent layouts stay mounted during navigation. Dynamic params. Catch-all routes. Error boundaries per route.
Loaders
Server-only data functions. Run in parallel. Full TypeScript inference from loader return type to component props.
Actions + Forms
Mutations via <Form>. Works without JavaScript. After every action, all data automatically revalidates.
Islands
On static routes, opt into interactivity per-component with <Island>. No framework runtime shipped.

One file. Loader fetches data. Action handles the form. Component renders it. Error boundary catches failures. TypeScript connects everything.

routes/app/projects/[id].tsx
import type { LoaderArgs, ActionArgs } from "@neutron-build/core";

export const config = { mode: "app" };

export async function loader({ params, context }: LoaderArgs) {
  const project = await context.db.getProject(params.id);
  if (!project) throw new Response("Not found", { status: 404 });
  return { project };
}

export async function action({ request, params, context }: ActionArgs) {
  const form = await request.formData();
  await context.db.updateProject(params.id, {
    name: form.get("name") as string,
  });
  return { saved: true };
}

export default function ProjectPage() {
  const { project } = useLoaderData<typeof loader>();
  const action = useActionData<typeof action>();

  return (
    <div>
      <h1>{project.name}</h1>
      {action?.saved && <p>Saved.</p>}
      <Form method="post">
        <input name="name" defaultValue={project.name} />
        <button type="submit">Save</button>
      </Form>
    </div>
  );
}

export function ErrorBoundary() {
  const error = useRouteError();
  if (isRouteErrorResponse(error)) {
    return <p>{error.status}: Not found.</p>;
  }
  return <p>Something went wrong.</p>;
}
typeof loader → useLoaderData → component. End-to-end type safety. No codegen. No runtime validation.

Navigate from /app/dashboard to /app/settings. The root layout stays. The app layout stays. Only the page swaps. No re-render. No flash. No lost state.

src/routes/
  _layout.tsx            ← stays mounted
  app/
    _layout.tsx          ← stays mounted
    dashboard.tsx        ← unmounts
    settings.tsx         ← mounts
    projects/
      _layout.tsx        ← stays mounted between projects
      [id].tsx

Static routes ship zero JavaScript. When you need interactivity, wrap a component in <Island>. Only that component hydrates. Everything else stays as HTML.

routes/index.tsx
import { Island } from "@neutron-build/core";
import Counter from "../components/Counter";

export default function Home() {
  return (
    <main>
      <h1>Welcome</h1>
      <p>This is HTML. No JavaScript.</p>

      <Island component={Counter} client="visible" start={0} />

      <footer>Also HTML. Also no JavaScript.</footer>
    </main>
  );
}
client="load"Hydrate immediately.
client="visible"Hydrate when scrolled into view.
client="idle"Hydrate when the browser is idle.
client="media"Hydrate when a media query matches.
NeutronNext.jsRemixAstroSvelteKitNuxtSolidStart
Static routesZero JSRequires ReactRequires ReactZero JSSvelte runtimeRequires VueSolid runtime
App routesPreact or ReactRequires ReactRequires ReactLimitedSvelteVueSolid
Client runtime3 KB (Preact)~42 KB~42 KB0 KB~2 KB~30 KB~8 KB
Data loadingParallel loadersParallel (App Router)Parallel loadersAstro.globload()useFetchcreateResource
Data layerDB + Cache + QueueDIYDIYDIYDIYDIYDIY
MutationsActions + FormServer ActionsActions + FormLimitedForm actionsServer routesActions
Nested layoutsYesYes (App Router)YesYesYesYesYes
IslandsYesNoNoYesNoNoNo
Rendering modes2 (explicit)5+ (implicit)SSR + pre-renderSSG + SSR + hybridSSR + SSG + hybridSSR + SSG + hybridSSR + SSG
Deploy targetsAny (adapters)Vercel-biasedAnyAny (adapters)Any (adapters)Any (presets)Any (adapters)

Neutron takes inspiration from the best ideas across these frameworks. Every framework makes trade-offs — Neutron makes different ones.

Benchmarks across 8 scenarios. Production builds. Same hardware. autocannon with 80 concurrent connections.

~18,500
Avg Requests/sec
Across 8 scenarios
~22,000
Static RPS
GET / , node track
~2.7x
Faster than Next.js
vs ~6,800 RPS average
~1.7x
Faster than Astro
vs ~11,100 RPS average
Neutron
~18,500 RPS avg
Neutron (React compat)
~19,500 RPS avg
Astro
~11,100 RPS avg
Next.js
~6,800 RPS avg
Remix 3 (RR7)
~5,200 RPS avg

Framework Comparison

Average performance across 8 benchmark scenarios. Production builds, same hardware.

FrameworkAvg RPSAvg Latencyvs Neutron
Neutron (Preact)18,510~8msBaseline
Neutron (React Compat)19,538~8ms+6%
Astro 511,140~11ms-40%
Next.js 156,762~18ms-63%
Remix 26,158~18ms-67%
Remix 3 (RR7)5,185~24ms-72%

autocannon, 80 concurrent connections, 5s duration, production builds, node track. Representative single run — figures vary run-to-run. Reproducible harness.

Why Neutron is Faster

The performance gap comes from architecture, not tricks:

Static routes skip the render pipeline. No React, no virtual DOM, no component tree. A static route is string concatenation. This is why static pages hit ~22,000 RPS while Next.js serves ~10,800.

App routes use Preact instead of React. Preact's server render is lighter — 3 KB vs ~42 KB. That means faster SSR and smaller bundles. Your database will still be the bottleneck, but you'll have more headroom.

What This Means in Practice

A ~2.7x throughput advantage over Next.js means you can serve the same traffic with fewer servers. For most applications, the database is the real bottleneck — but when you do need to scale horizontally, that headroom matters.

All benchmarks are production builds tested with autocannon (80 concurrent connections, 5s duration). The full harness — every scenario, every framework, reproducible — is open on GitHub. We encourage you to run it on your own hardware.

MetricNeutronNext.jsRemixAstroSvelteKitNuxtSolidStart
Static page JS0 KB~85 KB~40 KB0 KB~22 KB~55 KB~18 KB
App page JS~3 KB~90 KB~45 KBN/A~24 KB~60 KB~22 KB
Client runtime~3 KB (Preact)~42 KB (React)~42 KB (React)0 KB~2 KB~30 KB (Vue)~8 KB (Solid)
IslandsYesNoNoYesNoNoNo
Server runtimeBun or Node.jsNode.jsNode.jsNode.jsNode.jsNode.js / NitroNode.js

Bundle sizes are approximate and based on default configurations. Neutron ships Preact (~3 KB gzipped) by default. React-compat mode available for full React ecosystem compatibility.

Get started

$npm create neutron@latest
cd my-appnpm run dev
Read the docs →