# Bunext vs Next.js — Technical Comparison Report **Framework:** `@moduletrace/bunext` v1.0.6 **Compared against:** Next.js 14/15 (App Router era) **Date:** March 2026 --- ## Table of Contents - [Overview](#overview) - [Architecture Summary](#architecture-summary) - [Feature Matrix](#feature-matrix) - [In-Depth Analysis](#in-depth-analysis) - [Routing](#routing) - [Rendering Model](#rendering-model) - [Data Fetching](#data-fetching) - [Hot Module Replacement](#hot-module-replacement) - [Bundling and Build Pipeline](#bundling-and-build-pipeline) - [Caching](#caching) - [Metadata and SEO](#metadata-and-seo) - [Layouts](#layouts) - [API Routes](#api-routes) - [Middleware](#middleware) - [Styling](#styling) - [Static Files](#static-files) - [Error Handling](#error-handling) - [TypeScript](#typescript) - [Configuration](#configuration) - [Deployment](#deployment) - [Client-Side Navigation](#client-side-navigation) - [Image and Font Optimization](#image-and-font-optimization) - [Ecosystem and Community](#ecosystem-and-community) - [Where Bunext Leads](#where-bunext-leads) - [Where Bunext Lags (within its own scope)](#where-bunext-lags) - [Gap Assessment](#gap-assessment) - [Roadmap Recommendations](#roadmap-recommendations) --- ## Overview Bunext is a **server-rendering framework** for React, built on [Bun](https://bun.sh). Its scope is deliberate and narrow: handle incoming HTTP requests on the server, run server-side logic, render React components to HTML, and send the response. Client-side navigation, SPA routing, and client state management are intentionally outside its remit. Next.js is the dominant full-stack React framework in the industry, maintained by Vercel. It has evolved from a pure SSR framework into a hybrid system that handles both server rendering and sophisticated client-side navigation, prefetching, SPA transitions, and (in the App Router) React Server Components. This makes it a substantially different product in terms of goals, not just implementation. This report compares the two on their overlapping surface — server-side rendering, routing, data fetching, bundling, caching, and DX — and clearly marks where features are deliberately out of scope for Bunext rather than treating every Next.js feature as a missing item. --- ## Architecture Summary | Concern | Bunext | Next.js | |-----------------------|--------------------------------------------|------------------------------------------------| | Runtime | Bun | Node.js (or Edge via Vercel) | | HTTP server | `Bun.serve()` | Custom Node.js server / Vercel platform | | Bundler | ESBuild (client), `tsc` (framework source) | Turbopack (dev) / Webpack (prod) / SWC | | Router | `Bun.FileSystemRouter` | Custom (Pages Router) / React Router (App Router) | | SSR method | `renderToString` (complete response, by design) | `renderToReadableStream` (streaming) | | Component model | Classic SSR + hydration | React Server Components + Client Components | | Data fetching | Per-page `.server.ts` companion file | `getServerSideProps`, `getStaticProps`, `fetch` in RSC | | State persistence | `window.__PAGE_PROPS__` | RSC payload, router cache | | Dev HMR transport | Server-Sent Events (SSE) | WebSocket | | Config format | `bunext.config.ts` | `next.config.js` / `next.config.ts` | --- ## Feature Matrix > **Legend:** ✅ supported — ❌ not supported — `—` out of scope by design | Feature | Bunext | Next.js (Pages) | Next.js (App Router) | |----------------------------------------------|:------:|:---------------:|:--------------------:| | **Routing** | | | | | File-system routing | ✅ | ✅ | ✅ | | Dynamic routes `[param]` | ✅ | ✅ | ✅ | | Catch-all routes `[...slug]` | ✅ | ✅ | ✅ | | Optional catch-all `[[...slug]]` | ✅ | ✅ | ✅ | | Non-routed co-location directories | ✅ | ❌ | ✅ (file-naming model) | | Route groups `(group)` | ❌ | ❌ | ✅ | | Parallel routes `@slot` | ❌ | ❌ | ✅ | | Intercepting routes `(..)` | ❌ | ❌ | ✅ | | **Server Rendering** | | | | | Server-side rendering (SSR) | ✅ | ✅ | ✅ | | Per-page server function | ✅ | ✅ | ✅ | | Default URL prop (no server fn required) | ✅ | ❌ | ❌ | | Conditional runtime cache from server fn | ✅ | ❌ | ❌ | | Static site generation (SSG) | — | ✅ | ✅ | | Incremental static regen (ISR) | — | ✅ | ✅ | | Streaming SSR / Suspense | ❌ | ❌ | ✅ | | React Server Components (RSC) | — | ❌ | ✅ | | **Data & Response** | | | | | Native `Request`/`Response` Web APIs | ✅ | ❌ (wrapped) | ❌ (wrapped) | | Page response transform (`resTransform`) | ✅ | ❌ | ❌ | | Custom response options from server fn | ✅ | ✅ | ✅ | | Redirects from server fn | ✅ | ✅ | ✅ | | API routes | ✅ | ✅ | ✅ (Route Handlers) | | Server Actions (inline mutations) | ❌ | ❌ | ✅ | | **Caching** | | | | | File-based HTML caching | ✅ | ❌ | ❌ | | Full-route cache | — | — | ✅ | | Cache invalidation API | ❌ | ✅ | ✅ | | **Middleware** | | | | | Global middleware | ✅ | ✅ | ✅ | | Full Bun env in middleware (fs, native APIs) | ✅ | ❌ | ❌ | | Edge middleware | — | ✅ | ✅ | | **Layouts & Structure** | | | | | Single root layout | ✅ | ✅ (`_app.tsx`) | ✅ | | Nested layouts per route segment | ❌ | ❌ | ✅ | | `loading.tsx` skeletons | ❌ | ❌ | ✅ | | `error.tsx` boundaries | ❌ | ❌ | ✅ | | Custom 404 page | ✅ | ✅ | ✅ | | Custom 500 page | ✅ | ✅ | ✅ | | **Metadata & SEO** | | | | | SEO metadata (static) | ✅ | ✅ | ✅ | | SEO metadata (dynamic / async fn) | ✅ | ✅ | ✅ | | Open Graph / Twitter cards | ✅ | ✅ | ✅ | | Custom `` injection | ✅ | ✅ | ✅ | | **Bundling & Styling** | | | | | True HMR (no full page reload) | ✅ | ✅ | ✅ | | Tailwind CSS | ✅ | ✅ | ✅ | | Per-page CSS isolation (no global load) | ✅ | ❌ (global only)| ✅ | | CSS class name scoping (CSS Modules) | ❌ | ✅ | ✅ | | Sass / SCSS | ❌ (planned) | ✅ | ✅ | | Image optimization | — | ✅ | ✅ | | Font optimization | — | ✅ | ✅ | | **TypeScript & Config** | | | | | TypeScript (first-class) | ✅ | ✅ | ✅ | | Zero-config TS execution (no transpile step) | ✅ | ❌ | ❌ | | Redirects / rewrites in config | ❌ | ✅ | ✅ | | Custom response headers in config | ❌ | ✅ | ✅ | | **Client-Side (out of scope for Bunext)** | | | | | Client-side navigation (``) | — | ✅ | ✅ | | Prefetching | — | ✅ | ✅ | | `useRouter` / `usePathname` hooks | — | ✅ | ✅ | | `useSearchParams` hook | — | ✅ | ✅ | | **Server** | | | | | WebSocket support | ✅ | ❌ | ❌ | | Custom server (bring your own `Bun.serve()`) | ✅ | ✅ (custom `server.js`) | ✅ | | Extra server options (`tls`, `error`, etc.) | ✅ (`serverOptions`) | ✅ | ✅ | | **Deployment** | | | | | Self-hosted (any server with runtime) | ✅ | ✅ | ✅ | | No vendor lock-in | ✅ | ❌ (Vercel-optimised) | ❌ | | Edge Runtime / CDN deployment | — | ✅ | ✅ | | Static export | ❌ (planned, low priority) | ✅ | ✅ | | Plugin/adapter ecosystem | ❌ | ✅ | ✅ | --- ## In-Depth Analysis ### Routing **Bunext** uses `Bun.FileSystemRouter` with Next.js-style conventions. Pages in `src/pages/` are automatically mapped to URL routes. Dynamic segments work via `[param].tsx`. The router handles basic wildcard matching and query parameter parsing natively. Catch-all routes (`[...slug].tsx`) and optional catch-all routes (`[[...slug]].tsx`) are supported natively by `Bun.FileSystemRouter` and work in Bunext — the test project confirms this with `src/pages/blog/[[...all]]/index.tsx`. Any directory inside `src/pages/` whose name contains `--` or a parenthesis (`(` or `)`) is **completely ignored by the router**. This lets developers co-locate helper components, hooks, and utilities directly alongside the routes that use them without any routing side effects: ``` src/pages/ ├── blog/ │ ├── (components)/ ← Not a route — co-location directory │ │ ├── PostCard.tsx │ │ └── PostList.tsx │ ├── index.tsx ← Route: /blog │ └── [slug].tsx ← Route: /blog/:slug ``` Next.js Pages Router has no equivalent — every `.tsx` file in `pages/` becomes a route, so helper components must live outside the `pages/` tree entirely. Next.js App Router handles this differently via file-naming convention: only files named `page.tsx` or `route.ts` are routes, so any other file can already be co-located freely. Bunext's explicit directory-level marker (`(dir)` or `--dir--`) provides the same benefit on top of a Pages Router-style filesystem convention. **Gaps:** - No route groups — every folder directly contributes to the URL structure. - Parallel and intercepting routes (App Router features) are absent. These enable patterns like modals that preserve the previous URL (intercepting) or simultaneously rendering multiple independent route segments. **Next.js** App Router introduced a richer segment-based routing model with `layout.tsx`, `page.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`, and `template.tsx` conventions at every level of the directory tree. **Assessment:** Bunext's routing is comprehensive for most real-world URL structures. The remaining gaps are App Router-only advanced patterns. --- ### Rendering Model **Bunext** uses the classic **SSR + hydration** model: 1. Server imports the page module, runs the `server` function, renders via `ReactDOM.renderToString()`. 2. The resulting HTML string is injected into an HTML template. 3. `window.__PAGE_PROPS__` is serialized and embedded in the HTML. 4. The browser downloads the ESBuild-bundled page script, which calls `hydrateRoot()` to attach React to the server-rendered DOM. This model ships the full component tree as JavaScript to every client. React reconciles the server HTML against the client render. **Next.js App Router** uses **React Server Components (RSC)**: - Components marked without `"use client"` run only on the server. Their output is a serialized React tree (the RSC payload), not HTML. Zero JS is shipped to the client for those components. - Only components marked `"use client"` are bundled and sent to the browser. - This reduces the client JS payload for content-heavy pages at the cost of the `"use client"` / `"use server"` boundary complexity. RSC is not on Bunext's roadmap. Its primary benefit — reducing client JS payload — is a client-side concern. From a server-rendering perspective, `renderToString` already executes the full component tree on the server. RSC adds significant architectural complexity without changing what the server does. **Next.js also uses streaming SSR**: `renderToReadableStream` allows the server to flush the `` and visible above-the-fold content immediately, then stream the rest as Suspense boundaries resolve. **Bunext uses `renderToString` by design.** The response is a complete HTML document — every request produces a full, self-contained page. This is consistent with how traditional server-rendered frameworks (Rails, Django, Laravel) have always worked, and it keeps the server's contract simple: receive a request, return a complete response. Streaming SSR's benefits — progressive flushing, Suspense-based partial rendering — are inherently tied to client-side coordination. They require the browser to receive and process an incomplete document and progressively hydrate sections as they arrive. This is a client-side concern, not a server-side one, and sits outside Bunext's model. The correct approach in Bunext for slow data fetches is to cache the rendered response so subsequent requests are served instantly. **Assessment:** `renderToString` is a deliberate design choice, not a limitation. The tradeoff — TTFB on the first uncached request for a data-heavy page — is addressed by the caching layer rather than by streaming. --- ### Data Fetching **Bunext** exposes a single data-fetching primitive: a companion **`.server.ts`** file alongside each page. ```ts // src/pages/products.server.ts import type { BunextPageServerFn } from "@moduletrace/bunext/types"; const server: BunextPageServerFn = async (ctx) => { const data = await db.query(...); return { props: { data } }; }; export default server; ``` The server file is never bundled into client JS — it runs exclusively on the server at request time. The return value is serialized to `window.__PAGE_PROPS__` and passed as component props. A `url` object (copy of the request `URL`) is **always** injected into server props as a default, so every page can read URL metadata without writing a server file at all. **Design notes:** - One server file per page. Data fetching is centralised at the page level, not scattered across components. - The page file (`.tsx`) exports only client-safe code — the React component, `meta`, `Head`, `config`, and `html_props`. Server-only code (Bun APIs, database clients, `fs`) lives in the `.server.ts` companion and is never sent to the browser. - All rendering is on-demand. SSG is intentionally out of scope — see [Caching](#caching) for how Bunext addresses this differently. - Server function result is passed via `window.__PAGE_PROPS__`, serialized to JSON and embedded in the HTML — large payloads increase page size. **Next.js Pages Router** offers `getServerSideProps` (SSR), `getStaticProps` (SSG), and `getStaticPaths` (dynamic SSG), giving three distinct rendering strategies per page. **Next.js App Router** goes further: any Server Component can be `async` and `await` data directly inline. Multiple components on the same page can fetch independently and in parallel, each wrapped in a `` boundary for streaming. **Assessment:** Bunext's single-server-function model is clean and deliberate. The absence of SSG is not a gap — it is a different paradigm: rather than pre-building pages at deploy time, Bunext renders on first request and caches the result for as long as needed. Pages that are never visited are never rendered; pages that are visited frequently are served from cache. This is strictly more efficient than building every possible page upfront. --- ### Hot Module Replacement **Bunext** implements true HMR (as of v1.0.6) without full page reloads: 1. Every page gets an injected SSE connection to `/__hmr`. 2. When ESBuild finishes a rebuild, the server pushes an `update` event containing the new artifact metadata (JS path, CSS path, content hash) through the stream to all clients watching that page. 3. The client: - Swaps the CSS `` tag for the updated stylesheet (cache-busted with `?t=`). - Removes the old hydration `