Developing hybrid rendering (SSR + CSR) for a web application
Hybrid rendering — different parts of the application or different routes use different rendering strategies based on requirements. Public pages with SEO render server-side, closed dashboards client-side, landing pages statically. All within one application.
This is not a compromise of "a bit of each" — this is precise architectural tuning for requirements of each product part.
Hybrid rendering models
Route-level rendering: each route declares its strategy. Next.js, Nuxt 3 and SvelteKit natively support this.
Component-level rendering: Server Components render server-side, Client Components client-side. The boundary passes within a single route.
Segment-level rendering: header and navigation are static, main content is SSR, widgets (chat, notifications) are CSR.
Configuration in Next.js App Router
// next.config.ts — routeRules / export config
export const revalidate = 3600; // Default: ISR with 1 hour TTL
Each route segment manages rendering independently:
app/
(marketing)/ # Group without layout effect
page.tsx # SSG — static home
about/page.tsx # SSG
blog/
page.tsx # ISR — post list
[slug]/page.tsx # ISR — post
(app)/
layout.tsx # Server layout with session check
dashboard/
page.tsx # SSR — dashboard with real data
reports/
page.tsx # CSR — heavy charts, client only
// app/(app)/reports/page.tsx — force CSR
'use client'; // Entire route as client component
import dynamic from 'next/dynamic';
// Heavy libraries on client only
const RechartsChart = dynamic(() => import('@/components/charts/RechartsChart'), {
ssr: false,
loading: () => <ChartSkeleton />,
});
Configuration in Nuxt 3
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Public site
'/': { prerender: true },
'/about': { prerender: true },
'/blog/**': { isr: 3600 },
// API and application
'/api/**': { cors: true },
'/app/**': { ssr: false }, // SPA for closed sections
'/admin/**': { ssr: true }, // SSR for admin panel (SEO not needed, but fast first load)
},
});
Server and Client Components — rendering boundary
React Server Components (Next.js App Router) — key to hybrid design at component level:
// Server component — query database directly, no API round-trip
// app/products/page.tsx
import { db } from '@/lib/db';
import { ProductCard } from './product-card'; // Server
import { FilterBar } from './filter-bar'; // Client
export default async function ProductsPage({
searchParams
}: {
searchParams: { category?: string; sort?: string }
}) {
// This code runs only on server
const products = await db.product.findMany({
where: { category: searchParams.category },
orderBy: { [searchParams.sort ?? 'name']: 'asc' },
include: { images: { take: 1 } },
});
return (
<div>
{/* Client component — manages filters via URL */}
<FilterBar initialCategory={searchParams.category} />
{/* Server component — just renders data */}
<div className="grid grid-cols-3 gap-4">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}
// filter-bar.tsx — client component
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
export function FilterBar({ initialCategory }: { initialCategory?: string }) {
const router = useRouter();
const params = useSearchParams();
function setCategory(category: string) {
const newParams = new URLSearchParams(params);
newParams.set('category', category);
router.push(`?${newParams.toString()}`);
}
return (
<nav>
{CATEGORIES.map(cat => (
<button key={cat.id} onClick={() => setCategory(cat.id)}>
{cat.name}
</button>
))}
</nav>
);
}
Passing data between Server and Client components
Server Components can't accept functions from Client Components — only serializable data:
// Correct: server data as props to client component
export default async function Page() {
const user = await getUser(); // Server fetch
return <UserProfile user={user} />; // user is serializable object
}
// Wrong: can't pass function from Server to Client
// return <Button onClick={serverFunction} /> // Compilation error
For server mutations use Server Actions:
// actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function updateProduct(id: string, data: Partial<Product>) {
await db.product.update({ where: { id }, data });
revalidatePath(`/products/${id}`); // Invalidate cache
}
// Client component calls server action directly
'use client';
import { updateProduct } from './actions';
function EditForm({ product }: { product: Product }) {
return (
<form action={async (formData) => {
'use server'; // Inline server action
await updateProduct(product.id, {
name: formData.get('name') as string,
});
}}>
<input name="name" defaultValue={product.name} />
<button type="submit">Save</button>
</form>
);
}
Streaming and Suspense in hybrid architecture
// Parallel loading of independent server data
import { Suspense } from 'react';
export default function DashboardPage() {
return (
<div>
{/* Renders immediately — doesn't wait for data */}
<DashboardHeader />
{/* Each block streams independently */}
<Suspense fallback={<StatsSkeleton />}>
<Stats /> {/* async component with fetch */}
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</div>
);
}
User sees header instantly, blocks appear as data becomes ready — without blocking entire page.
Metrics and trade-offs
| Strategy | TTFB | TTI | SEO | Complexity |
|---|---|---|---|---|
| Pure SSR | High | Medium | Excellent | Medium |
| Pure CSR | Low | High | Poor | Low |
| Pure SSG | Minimal | Minimal | Excellent | Low |
| Hybrid (App Router) | Low | Low | Excellent | High |
Hybrid rendering is maximally efficient, but requires understanding the model — where server/client boundary passes, how cache works, how to avoid waterfall requests.
Implementation timeline
- Week 1–2: design server/client boundaries by routes, setup Next.js App Router or Nuxt 3
- Week 3: Server Components for public pages, Server Actions for mutations
- Week 4: Client Components for interactive parts, Suspense boundaries
- Week 5: ISR and caching for public content, TTFB optimization
- Week 6: testing, Core Web Vitals monitoring, deployment







