mono/packages/ui-next/docs/ssr.md
2026-04-11 01:48:32 +02:00

9.7 KiB

Server-Side Rendering (SSR) Strategy

Given ui-next's aggressive focus on runtime minimalism (Preact, zod/mini, source aliasing, < 50kB gzip bundle) and modern build tooling (Rolldown), any SSR solution must strictly adhere to the "no bloat" philosophy.

Traditional metaframeworks (Next.js, Remix, etc.) are explicitly ruled out as they force heavy runtime dependencies, prescriptive routing, and rigid Webpack/Turbopack tooling that break our highly tailored rolldown-vite build pipeline.

Here is an analysis of the most appealing SSR options moving forward, ranked from best aligned with our philosophy to least aligned.


1. Hand-Rolled Vite SSR + Hono with Optional SSG (The Zero-Bloat Path)

This approach combines Vite's native Server-Side Rendering API with a build-time pre-rendering step. It utilizes a micro-framework like Hono to serve requests, acting as a smart proxy that can toggle between Static Site Generation (SSG) and dynamic SSR on a per-route basis.

How it works (The Hybrid Architecture)

  • The Entry Point: You create an entry-server.tsx that exports a render function wrapping @tanstack/react-router and preact-render-to-string.
  • The SSG Build Step: A custom node script (prerender.js) runs after vite build. It walks through a list of known static, globally shared routes, calls entry-server.tsx to generate their HTML, and writes them to dist/[route]/index.html.
  • The Hono Server:
    • At runtime, Hono intercepts incoming requests.
    • Check Static (SSG): It first checks if a pre-rendered index.html file exists for the exact requested URL. If it does, it streams the raw HTML file back immediately.
    • Fallback Server Render (SSR): If the file doesn't exist (e.g., dynamically generated user widgets, authenticated portals, novel search parameters), Hono invokes the Vite SSR logic to parse the LayoutNode tree and render the string on-the-fly.

Pros

  • Absolute Best Performance: You get 0ms compute time for hit SSG pages (served like a CDN) while retaining full dynamic capabilities for interactive or authenticated routes via edge SSR.
  • Zero Black Box: Absolute control over exactly what gets imported and shipped. No magical framework boundaries.
  • Maintains Optimizations: Keeps our specific rolldown-vite setup and manual tree-shaking aliases intact, guaranteeing the < 50kb payload limit.
  • Micro-Runtime: The entire Hono logic fits tightly inside a Cloudflare Worker or single AWS Lambda.

Cons

  • Wiring Boilerplate: You have to manually manage the entry scripts, handle CSS collecting (to prevent FOUC), write the prerender.js script, and serialize/hydrate the useLayoutStore Zustand state securely.

2. Vike (formerly vite-plugin-ssr)

If manual Vite SSR wiring (Option #1) gets too tedious concerning hydration, CSS processing, and data fetching, Vike provides the missing glue without mutating the architecture. Note that Vike also supports an SSG export feature.

How it works

  • Drop in the vike plugin into vite.config.ts.
  • It defines simple +Page.server.tsx equivalents without stripping away your control of the backend node entry point.

Pros

  • "Do one thing, do it well": It strictly handles SSR routing and HTML streaming. You still own your TanStack Router and Preact setup.
  • Keeps Vite: Designed from the ground up for Vite without heavy macro-abstractions.

Cons

  • Slight overhead: Adds a dependency boundary and some minimal runtime footprint to coordinate the PageContext on the client.

3. TanStack Start

Given we are already heavily utilizing @tanstack/react-router, TanStack Start is the official SSR/Full-stack extension in the ecosystem.

How it works

  • Leverages Vinxi to handle multiple Vite environments (client, SSR, server functions) automatically.
  • Provides @server RPCs and native Suspense-based streaming out of the box.

The "Bloat" Warning

  • Vinxi runs on Nitro and significantly abstracts the base Vite configuration.
  • There's a high chance that our hyper-optimized manual source-aliasing: { find: "@tanstack/router-core", replacement: nm("@tanstack/router-core/src/index.ts") } ...would silently break or become incredibly difficult to manage across the split Node SSR bundle and browser target chunk.
  • Verdict: While tempting for Developer Experience (DX), it fundamentally contradicts our current "hand-tuned bare metal" footprint goals.

Conclusion & Next Steps

For ui-next, pursuing Option 1: Hand-Rolled Vite SSR + Hono with Optional SSG is the primary recommendation.

  1. Intelligent Caching: The hybrid SSR/SSG pattern allows us to cache high-traffic, non-authenticated widget layouts rigidly on disk/edge, while dropping down to live SSR for everything else.
  2. Hono: Allows us to decouple the rendering logic from Node-specific primitives, ensuring it runs efficiently on edge runtimes (Cloudflare Workers, Deno Deploy).
  3. Budget Compliance: The preact-render-to-string library respects our bundle constraints, and omitting a metaframework preserves full authority over the rolldown-vite pipeline.
  4. Hydration Integrity: We can safely inject dehydrated useLayoutStore layouts and widgetRegistry manifests directly into the HTML payload without hidden framework overhead manipulating our data modeling.

Usage Guide: Developing with SSR

With Hand-Rolled Vite SSR now integrated recursively into the backend, the local development workflow has changed. You no longer run two isolated development servers (one for UI and one for the API).

Starting the Server

To start development, execute the server process from the server-next package:

cd packages/server-next
npm run dev

Navigating the App

Visit http://localhost:3333. This is your server-next instance running Hono.

  • Requests matching /api/* are handled dynamically by Hono controllers.
  • All frontend requests (like /, /examples, or retrieving CSS) are safely pushed through Vite's Connect middleware instantiated natively inside the Node.js server.

4. Pure Model-to-String Serialization (The "Zero-JS" Server)

Instead of booting Preact/React on the server, we treat the LayoutNode tree as a recursive data structure and transform it into HTML strings using a pure TypeScript visitor.

How it works

  • The Transformer: A specialized function like renderLayout(root: LayoutNode): string that maps widget types (flex, grid, image) directly to HTML string snippets using template literals.
  • The Response: Hono serves the generated string instantly.
  • Client Hand-off: The HTML is delivered with data-node-id attributes. The client-side Preact app boots, consumes the JSON model from a <script> tag, and performs "Islands-style" hydration on the existing DOM nodes.

Pros

  • Fastest Possible Rendering: Faster than any component-based SSR because there is no Virtual DOM or hook orchestration on the server.
  • Zero Memory Pressure: No complex React internal state to track during requests.
  • Total Control: You define exactly how a flex node looks as raw HTML without framework-injected wrappers.

Cons

  • Logic Duplication: You may have to define the rendering logic twice (once as a string template for the server, once as a Preact component for the client) unless you use a shared template literal library.

5. Hono Native JSX (The Middle Ground)

Leverage Hono's Native JSX engine for the server-side pass.

How it works

  • You write your NodeRenderer in standard JSX.
  • On the server, you import jsx from hono/jsx. It compiles directly to string concatenations at build/runtime.
  • On the client, you use Preact as usual.

Pros

  • Consistent DX: Keep using JSX for both server and client.
  • No VDom overhead: Unlike preact-render-to-string, Hono JSX doesn't try to mock a DOM environment; it just outputs strings.

Conclusion: Reconstructing the Render Pipeline

The failure of heavy metaframeworks like Vike highlights that ui-next is built differently. Because we own the Model (LayoutNode), we don't need a general-purpose SSR solution.


6. Framework-less Islands (The DIY Hard-Mode Path)

If we explicitly want to avoid the "magic" of metaframeworks (Astro, Qwik, etc.), we can implement Islands Architecture manually. This is the peak of "Sovereign Engineering."

How it works

  • The String Render: In Hono, you import your recursive NodeRenderer and call preact-render-to-string (or a pure string visitor) on your LayoutNode model.
  • Data Serialization: The model state is injected into the HTML as a JSON script tag (<script id="initial-state">).
  • Targeted Hydration: Instead of hydrating the entire root, the client-side JS scans the DOM for specific widgets or interactive zones and calls preact.hydrate() only where necessary.

Pros

  • 100% Transparency: No hidden Vite plugins or internal framework state.
  • Total Performance: You only ship the JS required for the interactive chunks.
  • Portability: The logic is just standard TypeScript; it runs on the edge, in a browser, or in a worker with zero shims.

Final Conclusion: The Philosophy of ui-next

The failure of Vike and our rejection of Astro lead to a clear realization: ui-next is an Engine, not a Site.

Moving forward, we treat SSR as a pure Data-to-HTML transformation task performed by Hono, and Client UI as a Hydration task performed by Preact. This "Framework-less" path preserves our hyper-optimized bundle size and ensures the system remains unbreakable across future node/tooling migrations.