Skip to main content

Architecture Deep Dive

Vinext is a Vite plugin that reimplements the Next.js API surface on top of Vite’s infrastructure, with Cloudflare Workers as the primary deployment target. This guide explains the architectural decisions, implementation patterns, and internal mechanics.

Core Architecture

Plugin Architecture

Vinext is implemented as a standard Vite plugin that hooks into Vite’s lifecycle:
  1. Resolves all next/* imports to local shim modules
  2. Scans pages/ and app/ directories to build file-system routes
  3. Generates virtual entry modules for RSC, SSR, and browser environments
  4. Integrates with @vitejs/plugin-rsc for React Server Components
From packages/vinext/src/index.ts:

Separation of Concerns

What @vitejs/plugin-rsc handles:
  • Bundler transforms for "use client" / "use server" directives
  • RSC stream serialization (wraps react-server-dom-webpack)
  • Multi-environment builds (RSC/SSR/Client)
  • CSS code-splitting and auto-injection
  • HMR for server components
  • Bootstrap script injection for client hydration
What Vinext handles:
  • File-system routing (scanning app/ and pages/ directories)
  • Request lifecycle (middleware, headers, redirects, rewrites, route handling)
  • Layout nesting and React tree construction
  • Client-side navigation and prefetching
  • Caching (ISR, "use cache", fetch cache)
  • All next/* module shims

Request Flow

Pages Router Flow

App Router Flow

Multi-Environment Architecture

The RSC/SSR Environment Boundary

This is the single most important architectural detail. Understanding this prevents the most common bugs.
The RSC environment and SSR environment are separate Vite module graphs with separate module instances. If you set state in a module in the RSC environment (e.g., setNavigationContext() in next/navigation), the SSR environment’s copy of that module is unaffected. Rule of thumb: Any per-request state that "use client" components need during SSR must be explicitly passed from the RSC entry to the SSR entry via the handleSsr(rscStream, navContext) call. From packages/vinext/src/server/app-dev-server.ts:
The SSR entry receives this context and calls the setter in its own module instance before rendering client components.

Why This Architecture?

React Server Components require two separate render passes:
  1. RSC pass (server-only): Renders server components to an RSC stream
  2. SSR pass (client components): Hydrates client components from the RSC stream to HTML
Vite’s multi-environment system gives each pass its own module graph with the correct import conditions:
  • RSC environment uses react-server condition
  • SSR environment uses node condition
  • Client environment uses browser condition
This prevents mixing incompatible APIs (e.g., server-only code leaking to the client).

Virtual Module System

Virtual Module Resolution

Vinext generates several virtual modules that serve as entry points: Pages Router:
  • virtual:vinext-server-entry - SSR server entry
  • virtual:vinext-client-entry - Client hydration entry
App Router:
  • virtual:vinext-rsc-entry - RSC request handler
  • virtual:vinext-app-ssr-entry - SSR entry
  • virtual:vinext-app-browser-entry - Client hydration entry

Resolution Quirks

Virtual modules have no real file location, so all imports within them must use absolute paths.
Build-time root prefix: Vite prefixes virtual module IDs with the project root path when resolving SSR build entries. The resolveId hook must handle both:
  • virtual:vinext-server-entry
  • <root>/virtual:vinext-server-entry
\0 prefix in client environment: When the RSC plugin generates its browser entry, it imports virtual modules using the already-resolved \0-prefixed ID. Vite’s import-analysis plugin can’t resolve this. Fix: strip the \0 prefix before matching. From packages/vinext/src/index.ts:

File-System Routing

App Router Route Discovery

The App Router scanner (packages/vinext/src/routing/app-router.ts) walks the app/ directory to build route metadata:
Key features:
  • Route groups (group) are transparent (don’t affect URL)
  • Parallel routes @slot are discovered and tracked
  • Intercepting routes (.)/(..)/(...) are mapped to target patterns
  • Layout nesting is computed from file structure
  • Segment depths are calculated for hook support

Metadata Collection

Each route tracks per-layout error boundaries and not-found pages:
This enables proper error boundary nesting: errors from layout N are caught by the boundary at layout N-1, matching Next.js behavior.

Component Tree Construction

Layout Wrapping

The RSC entry builds the component tree by wrapping the page with layouts (innermost to outermost): From packages/vinext/src/server/app-dev-server.ts:

Parallel Slots

Parallel slots (@slot directories) are passed as named props to the layout at their directory level:

Production Builds

Multi-Environment Build Pipeline

You must use createBuilder() + builder.buildApp() for production builds, not build() directly.
Calling build() from the Vite JS API doesn’t trigger the RSC plugin’s multi-environment build pipeline. The build sequence:
  1. RSC environment build - Server components bundle
  2. SSR environment build - SSR runtime bundle
  3. Client environment build - Browser bundle with code splitting
  4. Manifest generation - Maps modules to chunks
  5. Asset optimization - CSS extraction, minification

Code Splitting Strategy

Vinext uses a conservative code-splitting strategy optimized for real-world performance: From packages/vinext/src/index.ts:
Why not per-package splitting?
  • Per-package splitting creates 50-200+ chunks (exceeds HTTP/2 sweet spot of ~25 requests)
  • Small files compress poorly (gzip/brotli restart with empty dictionary)
  • ES module evaluation has per-module overhead
  • Rollup’s graph-based splitting already handles shared dependencies well

Treeshaking Configuration

Vinext uses aggressive treeshaking for vendor packages:
The "no-external" setting means:
  • Local project modules: preserve side effects (CSS imports, polyfills)
  • node_modules packages: treat as side-effect-free unless exports are used
This is critical for large barrel-exporting libraries (mermaid, @mui/material, lucide-react) that re-export hundreds of sub-modules.

Next Steps

RSC Integration

Deep dive into React Server Components integration

Build Pipeline

Production build pipeline and optimization

Virtual Modules

Virtual module system and entry generation

API Reference

Complete API documentation