diff --git a/.changeset/green-pans-cheer.md b/.changeset/green-pans-cheer.md
new file mode 100644
index 00000000..0b1269cd
--- /dev/null
+++ b/.changeset/green-pans-cheer.md
@@ -0,0 +1,5 @@
+---
+'spiceflow': patch
+---
+
+add a production SSR HTML cache for fast React Server Component pages, keyed by a progressive hash of the flight stream and bounded by total byte size. The cache skips non-GET requests, responses with `Set-Cookie`, and can be disabled with `SPICEFLOW_DISABLE_SSR_CACHE=1` for benchmarking or debugging.
diff --git a/.changeset/late-bottles-bathe.md b/.changeset/late-bottles-bathe.md
new file mode 100644
index 00000000..b0313887
--- /dev/null
+++ b/.changeset/late-bottles-bathe.md
@@ -0,0 +1,5 @@
+---
+'spiceflow': patch
+---
+
+add an experimental `SPICEFLOW_SSR_CACHE_MODE=prehash` mode for benchmarking a cache path that hashes the RSC flight stream before SSR, and expand the node benchmark `/about` route so heavier JSX trees make SSR cache tradeoffs easier to measure.
diff --git a/nodejs-example/app/main.tsx b/nodejs-example/app/main.tsx
index 2c8f285d..bf52da9b 100644
--- a/nodejs-example/app/main.tsx
+++ b/nodejs-example/app/main.tsx
@@ -6,6 +6,76 @@ import { Link, ProgressBar, redirect } from 'spiceflow/react'
import { sql } from './db'
import { serveStatic } from 'spiceflow'
+const aboutStats = [
+ { label: 'Server components', value: 'Nested layouts + SSR' },
+ { label: 'Runtime targets', value: 'Node, Bun, Workers' },
+ { label: 'HTML strategy', value: 'Stream flight + inject payload' },
+ { label: 'Benchmark focus', value: 'SSR and hydration cost' },
+ { label: 'Data access', value: 'Direct Postgres queries' },
+ { label: 'Transport', value: 'Web standard Request/Response' },
+] as const
+
+const aboutSections = [
+ {
+ title: 'Why this benchmark exists',
+ body:
+ 'The goal is to measure how much work happens between a React Server Components payload and the final HTML that reaches the browser.',
+ },
+ {
+ title: 'What the page exercises',
+ body:
+ 'This route intentionally renders a larger tree with repeated cards, nested lists, and descriptive content so the benchmark stresses JSX creation and HTML output more than a tiny static paragraph would.',
+ },
+ {
+ title: 'Why the route stays deterministic',
+ body:
+ 'The content is static so cache hit rates are easy to reason about and benchmark results are not dominated by random per-request data differences.',
+ },
+ {
+ title: 'What still matters',
+ body:
+ 'Even with a cache, the shape of the component tree, the cost of SSR, and the amount of HTML written to the socket still affect throughput and latency.',
+ },
+] as const
+
+const aboutFeatureCards = [
+ 'Route matching with nested layouts',
+ 'React Server Components decode on the server',
+ 'HTML stream generation from the flight payload',
+ 'Inline flight payload injection for hydration',
+ 'Redirect and not-found error propagation',
+ 'Header preservation across SSR responses',
+ 'Benchmark toggles for cache modes',
+ 'Byte-bounded LRU caching for HTML output',
+ 'Progressive hashing of the RSC flight stream',
+ 'Streaming fallback when the response is slow',
+ 'Client bootstrap injection with Vite RSC',
+ 'Static asset serving beside the RSC app',
+] as const
+
+const aboutFaqs = [
+ {
+ question: 'Does this page query the database?',
+ answer:
+ 'No. The home page does. This route is intentionally deterministic so the benchmark can isolate rendering and caching behavior more clearly.',
+ },
+ {
+ question: 'Why so much markup?',
+ answer:
+ 'A tiny route makes it hard to see whether HTML-side optimizations matter. A larger tree increases the amount of JSX work and serialized HTML.',
+ },
+ {
+ question: 'Why compare against Next.js and Hono?',
+ answer:
+ 'They provide useful reference points: plain string HTML on one end and a popular RSC framework on the other.',
+ },
+ {
+ question: 'What should improve with a good cache?',
+ answer:
+ 'The ideal case is skipping expensive repeated work while preserving the existing streaming behavior for slow pages.',
+ },
+] as const
+
export const app = new Spiceflow()
.use(serveStatic({ root: './public' }))
.use(serveStatic({ root: './dist/client' })) // required to serve vite built static files
@@ -40,22 +110,74 @@ export const app = new Spiceflow()
})
.page('/about', async function About() {
return (
-
-
About
-
- This is a demo app built with{' '}
-
- Spiceflow
-
- , showcasing React Server Components with direct database queries from
- the component tree.
-
-
- Back to home
-
+
+
+
About
+
+ This is a demo app built with{' '}
+
+ Spiceflow
+
+ , showcasing React Server Components with direct database queries from
+ the component tree. This benchmark version intentionally renders more
+ JSX so the cost of SSR and HTML generation is easier to observe.
+
+ Repeated card markup increases the amount of JSX the server must
+ turn into HTML for every request in this benchmark route.
+
+
+ ))}
+
+
+
+
+
Benchmark FAQ
+
+ {aboutFaqs.map((faq) => (
+
+
{faq.question}
+
{faq.answer}
+
+ ))}
+
+
+
+
+
+ Back to home
+
+
)
})
diff --git a/nodejs-example/hono-baseline.ts b/nodejs-example/hono-baseline.ts
index 7b70c488..7a2210f9 100644
--- a/nodejs-example/hono-baseline.ts
+++ b/nodejs-example/hono-baseline.ts
@@ -7,18 +7,132 @@ import { serve } from '@hono/node-server'
const app = new Hono()
+const aboutStats = [
+ { label: 'Server components', value: 'Nested layouts + SSR' },
+ { label: 'Runtime targets', value: 'Node, Bun, Workers' },
+ { label: 'HTML strategy', value: 'Stream flight + inject payload' },
+ { label: 'Benchmark focus', value: 'SSR and hydration cost' },
+ { label: 'Data access', value: 'Direct Postgres queries' },
+ { label: 'Transport', value: 'Web standard Request/Response' },
+]
+
+const aboutSections = [
+ {
+ title: 'Why this benchmark exists',
+ body:
+ 'The goal is to measure how much work happens between a React Server Components payload and the final HTML that reaches the browser.',
+ },
+ {
+ title: 'What the page exercises',
+ body:
+ 'This route intentionally renders a larger tree with repeated cards, nested lists, and descriptive content so the benchmark stresses JSX creation and HTML output more than a tiny static paragraph would.',
+ },
+ {
+ title: 'Why the route stays deterministic',
+ body:
+ 'The content is static so cache hit rates are easy to reason about and benchmark results are not dominated by random per-request data differences.',
+ },
+ {
+ title: 'What still matters',
+ body:
+ 'Even with a cache, the shape of the component tree, the cost of SSR, and the amount of HTML written to the socket still affect throughput and latency.',
+ },
+]
+
+const aboutFeatureCards = [
+ 'Route matching with nested layouts',
+ 'React Server Components decode on the server',
+ 'HTML stream generation from the flight payload',
+ 'Inline flight payload injection for hydration',
+ 'Redirect and not-found error propagation',
+ 'Header preservation across SSR responses',
+ 'Benchmark toggles for cache modes',
+ 'Byte-bounded LRU caching for HTML output',
+ 'Progressive hashing of the RSC flight stream',
+ 'Streaming fallback when the response is slow',
+ 'Client bootstrap injection with Vite RSC',
+ 'Static asset serving beside the RSC app',
+]
+
+const aboutFaqs = [
+ {
+ question: 'Does this page query the database?',
+ answer:
+ 'No. The home page does. This route is intentionally deterministic so the benchmark can isolate rendering and caching behavior more clearly.',
+ },
+ {
+ question: 'Why so much markup?',
+ answer:
+ 'A tiny route makes it hard to see whether HTML-side optimizations matter. A larger tree increases the amount of JSX work and serialized HTML.',
+ },
+ {
+ question: 'Why compare against Next.js and Hono?',
+ answer:
+ 'They provide useful reference points: plain string HTML on one end and a popular RSC framework on the other.',
+ },
+ {
+ question: 'What should improve with a good cache?',
+ answer:
+ 'The ideal case is skipping expensive repeated work while preserving the existing streaming behavior for slow pages.',
+ },
+]
+
const html = `
About - Hono baseline
-
+
How is this not illegal?
-
+
About
-
+
This is a demo app built with Spiceflow, showcasing React Server Components
- with direct database queries from the component tree.
+ with direct database queries from the component tree. This benchmark version
+ intentionally renders more JSX so the cost of SSR and HTML generation is
+ easier to observe.
+ Repeated card markup increases the amount of JSX the server must
+ turn into HTML for every request in this benchmark route.
+
+
+ `).join('')}
+
+
+
+
Benchmark FAQ
+
+ ${aboutFaqs.map((faq) => `
+
+
${faq.question}
+
${faq.answer}
+
+ `).join('')}
+
+
Back to home
diff --git a/nodejs-example/nextjs-baseline/app/about/page.tsx b/nodejs-example/nextjs-baseline/app/about/page.tsx
index 0fa7ebb0..80d81398 100644
--- a/nodejs-example/nextjs-baseline/app/about/page.tsx
+++ b/nodejs-example/nextjs-baseline/app/about/page.tsx
@@ -3,6 +3,76 @@ import Link from 'next/link'
// Force dynamic rendering so RSC runs on every request (fair comparison)
export const dynamic = 'force-dynamic'
+const aboutStats = [
+ { label: 'Server components', value: 'Nested layouts + SSR' },
+ { label: 'Runtime targets', value: 'Node, Bun, Workers' },
+ { label: 'HTML strategy', value: 'Stream flight + inject payload' },
+ { label: 'Benchmark focus', value: 'SSR and hydration cost' },
+ { label: 'Data access', value: 'Direct Postgres queries' },
+ { label: 'Transport', value: 'Web standard Request/Response' },
+] as const
+
+const aboutSections = [
+ {
+ title: 'Why this benchmark exists',
+ body:
+ 'The goal is to measure how much work happens between a React Server Components payload and the final HTML that reaches the browser.',
+ },
+ {
+ title: 'What the page exercises',
+ body:
+ 'This route intentionally renders a larger tree with repeated cards, nested lists, and descriptive content so the benchmark stresses JSX creation and HTML output more than a tiny static paragraph would.',
+ },
+ {
+ title: 'Why the route stays deterministic',
+ body:
+ 'The content is static so cache hit rates are easy to reason about and benchmark results are not dominated by random per-request data differences.',
+ },
+ {
+ title: 'What still matters',
+ body:
+ 'Even with a cache, the shape of the component tree, the cost of SSR, and the amount of HTML written to the socket still affect throughput and latency.',
+ },
+] as const
+
+const aboutFeatureCards = [
+ 'Route matching with nested layouts',
+ 'React Server Components decode on the server',
+ 'HTML stream generation from the flight payload',
+ 'Inline flight payload injection for hydration',
+ 'Redirect and not-found error propagation',
+ 'Header preservation across SSR responses',
+ 'Benchmark toggles for cache modes',
+ 'Byte-bounded LRU caching for HTML output',
+ 'Progressive hashing of the RSC flight stream',
+ 'Streaming fallback when the response is slow',
+ 'Client bootstrap injection with Vite RSC',
+ 'Static asset serving beside the RSC app',
+] as const
+
+const aboutFaqs = [
+ {
+ question: 'Does this page query the database?',
+ answer:
+ 'No. The home page does. This route is intentionally deterministic so the benchmark can isolate rendering and caching behavior more clearly.',
+ },
+ {
+ question: 'Why so much markup?',
+ answer:
+ 'A tiny route makes it hard to see whether HTML-side optimizations matter. A larger tree increases the amount of JSX work and serialized HTML.',
+ },
+ {
+ question: 'Why compare against Next.js and Hono?',
+ answer:
+ 'They provide useful reference points: plain string HTML on one end and a popular RSC framework on the other.',
+ },
+ {
+ question: 'What should improve with a good cache?',
+ answer:
+ 'The ideal case is skipping expensive repeated work while preserving the existing streaming behavior for slow pages.',
+ },
+] as const
+
export default function About() {
return (
-
About
-
- This is a demo app built with Next.js, showcasing React Server
- Components for performance comparison.
-
+
+
About
+
+ This is a demo app built with Next.js, showcasing React Server
+ Components for performance comparison. This benchmark version
+ intentionally renders more JSX so the cost of SSR and HTML generation
+ is easier to observe.
+