Living Document Notice
Published 2026-09-10. The evolving architecture, revisions, and connected notes for this dispatch live in the Stax Digital Garden.
Ditching the Full-Stack SSR Monolith
Summary
Full-stack JavaScript frameworks such as Next.js and Remix couple server-side HTML rendering to client-side component hydration trees. On low-memory virtual private servers and small edge instances constrained to 512 MB or 1 GB of RAM, the memory footprint required to maintain React component trees, serialized JSON payload buffers, and runtime module caches repeatedly triggers out-of-memory (OOM) kernel terminations during traffic spikes.
Harbor adopts an alternative rendering model. By replacing full-stack hydration frameworks with Hono executing over Node or lightweight edge runtimes, the presentation layer emits raw HTML string responses with zero client-side hydration scripts. This architecture reduces process baseline memory from hundreds of megabytes to under 35 MB, eliminates hydration mismatches, and simplifies process supervision on resource-constrained nodes.
Memory Profiles: Hydration Frameworks vs Raw String Streaming
Modern full-stack frameworks execute dual rendering passes: the server renders HTML markup while serializing an identical JSON representation of the component tree into embedded <script> tags for the browser runtime to hydrate. This dual-pass model doubles per-request memory allocation on the server. V8 garbage collection must reconcile both the intermediate virtual DOM tree and serialized string representations before freeing memory back to the operating system.
When multiple concurrent requests hit a Next.js process on a 512 MB virtual machine, resident set size (RSS) expands rapidly. Garbage collection pauses introduce tail latency spikes (p99 > 850ms), and sustained concurrency pushes process memory past cgroup thresholds.
In contrast, Hono treats HTML generation as deterministic string concatenation or streaming buffer evaluation. Components evaluate once into raw byte buffers or UTF-8 strings. The V8 heap allocates short-lived string fragments that transition directly to the network socket buffer, allowing immediate heap reclamation without deep object graph traversals.
| Metric | Next.js 14 (Node Runtime) | Remix v2 (Node Runtime) | Harbor (Hono on Node / Bun) |
|---|---|---|---|
| Idle RSS Memory | 148 MB | 112 MB | 28 MB |
| Active RSS (100 concurrent reqs) | 410 MB - 520 MB (OOM risk) | 310 MB - 390 MB | 46 MB - 62 MB |
| Average Response Overhead | 42 KB (HTML + State JSON) | 34 KB (HTML + State JSON) | 6.2 KB (Pure HTML) |
| Node Baseline Cold Boot | 1.8s - 3.2s | 1.1s - 2.4s | 0.12s - 0.35s |
| Client JavaScript Transferred | 85 KB - 160 KB min/br | 65 KB - 110 KB min/br | 0 KB |
| Garbage Collection Pauses | Frequent (15ms - 45ms) | Moderate (10ms - 25ms) | Negligible (< 2ms) |
Minimalist Renderer Architecture with Hono
The Harbor rendering pipeline consumes structured JSON records from Directus or local markdown caches and emits complete semantic HTML documents without intermediate virtual DOM abstractions.
import { Hono } from "hono";
import { html } from "hono/html";
interface ArticleRecord {
id: string;
title: string;
slug: string;
content_html: string;
published_at: string;
}
const app = new Hono();
app.get("/posts/:slug", async (c) => {
const slug = c.req.param("slug");
const article = await fetchArticleBySlug(slug);
if (!article) {
return c.html(
html`<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Not Found</title></head>
<body><main><h1>Entry not found</h1></main></body>
</html>`,
404
);
}
// Direct string template evaluation; zero hydration payload injected
return c.html(
html`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${article.title}</title>
<link rel="stylesheet" href="/assets/style.css" />
</head>
<body>
<header>
<nav><a href="/">Harbor Index</a></nav>
</header>
<main>
<article>
<header>
<h1>${article.title}</h1>
<time datetime="${article.published_at}">${article.published_at.slice(0, 10)}</time>
</header>
<section class="prose">
${html([article.content_html])}
</section>
</article>
</main>
</body>
</html>`
);
});
async function fetchArticleBySlug(slug: string): Promise<ArticleRecord | null> {
const endpoint = process.env.DIRECTUS_INTERNAL_URL || "http://127.0.0.1:8055";
const res = await fetch(`${endpoint}/items/articles?filter[article_slug][_eq]=${encodeURIComponent(slug)}&limit=1`, {
headers: { Authorization: `Bearer ${process.env.DIRECTUS_READ_TOKEN}` }
});
if (!res.ok) return null;
const payload = await res.json();
return payload.data?.[0] ?? null;
}
export default app;
Linux Memory Verification and Process Supervision
To measure real-world allocation on production hosts, execute memory inspections directly against running container cgroups or host processes.
# Measure precise PSS (Proportional Set Size) across node processes
smem -P "(node|bun|harbor)" -c "pid user command pss rss vms" -s pss
# Inspect cgroup memory usage for the Harbor edge container
cat /sys/fs/cgroup/memory/docker/$(docker ps -qf "name=harbor_edge")/memory.usage_in_bytes | awk '{print $1/1024/1024 " MB"}'
# Execute load simulation verifying memory stability under 100 concurrent workers
wrk -t4 -c100 -d30s --latency http://127.0.0.1:3000/posts/ditching-the-full-stack-ssr-monolith