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.

~3,500
Avg Requests/sec
Across 8 scenarios
8,262
Peak RPS
Static pages
~4x
Faster than Next.js
vs ~830 RPS average
~5x
Faster than Astro
vs ~634 RPS average
Neutron
~3,500 RPS avg
Neutron (React)
~2,870 RPS avg
Next.js
~830 RPS avg
Astro
~634 RPS avg
Remix 3
~277 RPS avg

Framework Comparison

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

FrameworkAvg RPSAvg Latencyvs Neutron
Neutron (Preact)3,498~8msBaseline
Neutron (React Compat)2,872~10ms-18%
Next.js 15830~14ms-76%
Astro 5634~16ms-82%
Remix 2471~20ms-87%
Remix 3 (RR7)277~28ms-92%

autocannon, 80 concurrent connections, 5s duration, production builds. Full methodology.

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 8,262 RPS while Next.js hits 2,756.

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 ~4x throughput advantage 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). Full methodology and raw data available in the benchmark blog post. We encourage you to run the benchmarks 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 →