Server-Side Rendering and Streaming
This module addresses the challenges and solutions involved in rendering React components on the server, hydrating them on the client, and efficiently managing data fetching and UI updates using streaming and concurrent features such as Suspense. It focuses on enabling performant server-client transitions with partial hydration capabilities.
Purpose and Core Concepts
Server-Side Rendering (SSR) is a technique to render the initial HTML markup of a React application on the server. This provides faster initial page loads and better SEO. However, SSR alone does not solve all performance issues, especially for complex data fetching scenarios and interactive UI elements.
Streaming SSR enhances this by progressively sending HTML chunks to the client as soon as they are ready, improving perceived load times and enabling partial hydration of components. Partial hydration means only some parts of the UI hydrate and become interactive immediately, while others hydrate asynchronously or on-demand, often coordinated with Suspense boundaries.
This module leverages React's concurrent rendering and Suspense mechanism to:
Support partial hydration by deferring hydration of data-dependent UI until the data is ready.
Use Suspense boundaries to coordinate server-to-client data streaming and client hydration.
Ensure consistent UI states between server-rendered markup and client hydration phases.
Provide testing workflows to verify SSR and streaming behaviors.
How the Module Works
Partial Hydration with Suspense
Partial hydration is implemented by splitting the UI into Suspense boundaries, some of which suspend rendering until asynchronous data is available. On the server, React renders as much as possible and streams the rest when data resolves. On the client, Suspense boundaries coordinate hydration timing based on data readiness.
The key workflow involves:
Server Render:
Components fetch data and render initial markup. Suspense boundaries suspend rendering sections that depend on asynchronous data, allowing streaming of content progressively.Client Hydration:
The client starts hydration with the parts already rendered. Suspended boundaries show fallback UI until data resolves, then hydrate asynchronously.Data Fetching Coordination:
Data fetching hooks (e.g.,useSWR) integrate with Suspense, throwing promises to suspend rendering until data is available.Debugging and State Tracking:
Hooks likeuseDebugHistorytrack data changes and hydration states to assist debugging of hydration flows.
Example: Delayed Hydration in page.tsx
The e2e/site/app/partially-hydrate/page.tsx file demonstrates partial hydration using Suspense with a simulated 2-second delay promise:
let resolved = false
const susp = new Promise(res => {
setTimeout(() => {
resolved = true
res(true)
}, 2000)
})
export default function Page() {
if (!resolved) {
throw susp // Suspends rendering until the promise resolves
}
const { data } = useData() // Data hook that fetches or reads cached data
const debugRef = useDebugHistory(data, 'second history:')
return (
<div>
<div ref={debugRef}></div>
<>second data (delayed hydration):{data || 'undefined'}</>
</div>
)
}
The component throws the
susppromise initially, causing React Suspense to suspend rendering this part.Once the promise resolves, rendering continues and data is fetched/displayed.
This simulates a staggered hydration process where some UI parts hydrate after a delay, illustrating partial hydration.
Interaction with Other System Components
Data Fetching Hooks (
useData):
The hydration process depends on data fetching hooks that integrate with Suspense by throwing promises when data is not ready. This integration ensures the UI waits for data during SSR and hydrates properly on the client.Debugging Utilities (
useDebugHistory):
To monitor hydration and data consistency, the module uses hooks that log or expose the history of data states, helping to verify that server and client renderings match.Testing Framework (
stream-ssr.test.ts):
Automated tests validate SSR and streaming scenarios by loading pages with partial hydration and verifying the presence of expected UI states before and after hydration completes.React Suspense and Concurrent Rendering:
The module relies heavily on React's Suspense for data fetching coordination and concurrent rendering features to stream and hydrate UI chunks asynchronously.
Design Patterns and Unique Approaches
Suspense-Driven Data Fetching:
Instead of manual loading state management, the module uses the Suspense mechanism to declaratively suspend rendering when data is unavailable, simplifying component logic and enabling React's streaming SSR capabilities.Partial Hydration via Promise Throwing:
Components explicitly throw promises to delay rendering until data or conditions are met, a pattern that leverages React's concurrent rendering model for granular hydration control.Debugging via Custom Hooks:
TheuseDebugHistoryhook records rendering and data changes, providing insight into hydration order and data consistency, which is critical in complex SSR scenarios.Playwright-Based SSR Streaming Tests:
Tests use Playwright to simulate browser environments and verify that streamed SSR content hydrates correctly without errors, ensuring the robustness of streaming and partial hydration features.
Key Code References
Suspense Boundary with Delayed Hydration (from page.tsx)
if (!resolved) {
throw susp // Suspends rendering until promise resolves
}
This snippet is central to implementing partial hydration by suspending rendering until asynchronous conditions are met.
Debugging Data History Hook Usage
const debugRef = useDebugHistory(data, 'second history:')
This hook tracks the data changes during hydration stages, helping detect mismatches or hydration issues.
SSR Streaming Test Example (from stream-ssr.test.ts)
await page.goto('./partially-hydrate', { waitUntil: 'commit' })
await expect(page.getByText('first data:undefined')).toBeVisible()
await expect(page.getByText('second data (delayed hydration):undefined')).toBeVisible()
await expect(page.getByText('first data:SSR Works')).toBeVisible()
await expect(page.getByText('second data (delayed hydration):SSR Works')).toBeVisible()
This test validates that:
The initial server-rendered content shows placeholders (
undefined) before hydration.After hydration completes, the data values are updated to the expected results (
SSR Works).No errors occur during the streaming and hydration process.
Mermaid Flowchart: Partial Hydration Workflow
flowchart TD
A[Server Starts Rendering] --> B[Render Components]
B --> C{Data Ready?}
C -->|No| D[Throw Promise - Suspend Rendering]
C -->|Yes| E[Render with Data]
D --> F[Stream Partial HTML to Client]
E --> F
F --> G[Client Receives HTML]
G --> H[Hydrate Ready Components]
H --> I{Suspense Boundary Suspended?}
I -->|Yes| J[Show Fallback UI]
I -->|No| K[Show Hydrated Content]
J --> L[Await Data Resolution]
L --> H
K --> M[Full Hydration Complete]
This flowchart illustrates the interaction of server rendering with data readiness, streaming partial HTML, client hydration, and Suspense boundaries managing fallback UI and asynchronous hydration.
Summary
The Server-Side Rendering and Streaming module implements sophisticated hydration strategies leveraging React Suspense and concurrent rendering. It enables partial hydration by deferring UI hydration until data is ready, improving performance and user experience. Integrated debugging utilities and rigorous automated tests ensure the correctness and reliability of SSR streaming workflows.