Optimizing React Server Components with Partial Prerendering and TanStack Start
As we move into mid-2026, the shift toward React Server Components (RSC) has matured beyond experimental experimentation into production-grade architecture. However, a persistent challenge remains: the trade-off between the immediate visual feedback of Static Site Generation (SSG) and the data freshness of Server-Side Rendering (SSR).
TanStack Start has emerged as a powerful meta-framework that bridges this gap by providing a type-safe, full-stack foundation built on top of TanStack Router. One of its most potent features is the implementation of Partial Prerendering (PPR), which allows developers to combine static shells with dynamic islands in a single HTTP request.
The Problem: The Hydration and Data Waterfall
Traditionally, web applications forced a binary choice. You could pre-render a page at build time (SSG) for instant Time to First Byte (TTFB), but you sacrificed dynamic content. Alternatively, you could use SSR, which ensures fresh data but forces the user to wait for the slowest database query before the browser receives a single byte of HTML.
Even with standard RSC streaming, we often see a "pop-in" effect where the layout shifts as components resolve. Partial Prerendering solves this by allowing the server to send a static HTML skeleton immediately while keeping the connection open to stream dynamic chunks as they resolve on the server.
Architectural Overview of PPR in TanStack Start
TanStack Start utilizes Vinxi, a versatile bundler composer, to manage the complex orchestration of server and client assets. In a PPR workflow, the framework identifies components wrapped in Suspense boundaries. During the build process, it generates a static version of the page where these boundaries are replaced with placeholders (fallback UI).
When a request hits the server:
- The static shell is served immediately from the edge or CDN.
- The server begins executing the dynamic RSC logic for the suspended components.
- As data resolves, the server streams the rendered HTML for those components over the same connection.
Implementing PPR with Drizzle ORM
To see this in action, let's consider a dashboard that requires a static sidebar but dynamic, user-specific data. We will use Drizzle ORM for type-safe database access, as its headless nature fits perfectly within RSC environments.
1. Defining the Data Fetcher
First, we create a server function to fetch our dynamic data. TanStack Start's createServerFn provides a clean boundary for these operations.
import { createServerFn } from '@tanstack/start';
import { db } from './db';
import { orders } from './schema';
import { desc } from 'drizzle-orm';
export const getRecentOrders = createServerFn('GET', async () => {
// Artificial delay to simulate database latency
await new Promise((r) => setTimeout(r, 1000));
return await db.select().from(orders).orderBy(desc(orders.createdAt)).limit(5);
});
2. Creating the Dynamic Component
Next, we build the component that will be streamed. Note that we call our server function directly within the component.
async function OrderList() {
const data = await getRecentOrders();
return (
<ul className="space-y-2">
{data.map((order) => (
<li key={order.id} className="p-4 border rounded">
Order #{order.id} - {order.status}
</li>
))}
</ul>
);
}
3. Orchestrating the Page with Suspense
In the route file, we wrap the dynamic component in a Suspense boundary. TanStack Start uses this boundary as the marker for PPR.
import { Suspense } from 'react';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/dashboard')({
component: DashboardPage,
});
function DashboardPage() {
return (
<div className="flex">
<aside className="w-64">Static Sidebar</aside>
<main className="flex-1">
<h1>Dashboard</h1>
<Suspense fallback={<OrderListSkeleton />}>
<OrderList />
</Suspense>
</main>
</div>
);
}
Technical Trade-offs and Considerations
While PPR significantly improves perceived performance, it introduces specific engineering challenges that must be managed.
Connection Management
Because PPR keeps the HTTP connection open longer than a standard static request, you must ensure your infrastructure (Load Balancers, CDNs) supports long-lived streaming responses. Services like Vercel and Cloudflare Workers handle this natively, but custom Nginx configurations may require tuning of proxy_buffering and keepalive_timeout.
Error Handling
In a standard SSR model, if a data fetch fails, you can return a 500 status code. With PPR, the 200 OK status is sent with the static shell before the dynamic data fetch even begins. This means errors must be handled gracefully within the component using Error Boundaries or by returning "error states" as part of the streamed HTML.
Cache Invalidation
One of the most complex aspects of PPR is determining when to re-validate the static shell. If your sidebar links change, you need a new build or an Incremental Static Regeneration (ISR) trigger. TanStack Start integrates with Nitro's caching layer, allowing for fine-grained control over TTLs for both the static and dynamic portions of the response.
Why TanStack Start Over Next.js?
While Next.js popularized PPR, TanStack Start offers a unique advantage for TypeScript-heavy teams: Full-stack Type Safety.
Because TanStack Start is built on TanStack Router, the search params, loaders, and server functions share a single source of truth for types. If you change a database schema in Drizzle, the type error propagates all the way to your component's props and your router's search param validation. This level of integration reduces the "glue code" typically required to keep frontend and backend types in sync.
Conclusion
Partial Prerendering represents the current ceiling of web performance optimization. By leveraging TanStack Start's tight integration with Vinxi and React's streaming capabilities, we can deliver applications that feel as fast as static sites while maintaining the power of dynamic, server-side logic.
As you implement this pattern, focus on identifying the "critical path" of your UI—the elements the user needs to see immediately—and isolate heavy data requirements behind Suspense boundaries. This architectural discipline not only improves Core Web Vitals like Largest Contentful Paint (LCP) but also creates a more resilient user experience in high-latency environments.