Streaming SSR
Streaming SSR eliminates the tradeoff between fast paint and real content. The shell — chrome, navigation, above-the-fold layout — renders and streams immediately. Slower data-dependent segments arrive over the same connection without blocking the initial paint.
How it works
Without streaming, the server waits for all data to resolve before sending any HTML — slow queries block the entire response. Pulse splits the view into a shell (sent immediately) and deferred segments (sent as placeholders, then replaced when data resolves).
Deferred segments arrive as chunks of HTML over the same connection — no extra requests, no client-side JavaScript required to swap content in.
Enabling streaming
To use streaming, the view is an object of named segment functions rather than a single function. The spec declares which segments are in the shell and which are deferred:
export default {
route: '/dashboard',
state: {},
server: {
data: async (ctx) => ({
user: await auth.getUser(ctx.cookies.sessionId), // fast
feed: await db.feed.latest(), // slow
stats: await analytics.summary(ctx.params.id), // slow
}),
},
stream: {
shell: ['header', 'nav'], // rendered immediately
deferred: ['feed', 'stats'], // streamed when server data resolves
},
view: {
header: (state, server) => `
<header class="site-header">
<a href="/">Dashboard</a>
<span>Hello, ${server.user.name}</span>
</header>
`,
nav: () => `
<nav>
<a href="/dashboard">Home</a>
<a href="/settings">Settings</a>
</nav>
`,
feed: (state, server) => `
<section class="feed">
${server.feed.map(item => `<article>${item.title}</article>`).join('')}
</section>
`,
stats: (state, server) => `
<div class="stats">
<p>Page views: ${server.stats.views}</p>
<p>Conversions: ${server.stats.conversions}</p>
</div>
`,
},
}
Deferred placeholders
While deferred segments are loading, Pulse renders a <div id="pulse-slot-[name]"> placeholder in their place. When the segment resolves, the rendered HTML is appended to the stream and a small inline script swaps the placeholder content.
hydrate omitted).Server data and streaming
All server.data() calls resolve in a single async call before rendering begins. Streaming is about splitting the view — not about parallelising data fetching. For parallel data fetching, use Promise.all inside server.data():
server: {
data: async (ctx) => {
// Fetch in parallel — both requests run concurrently
const [feed, stats] = await Promise.all([
db.feed.latest(),
analytics.summary(),
])
return { feed, stats }
},
}
When to use streaming
| Scenario | Use streaming? |
|---|---|
| Page with fast server data (< 20ms) | No — standard SSR is simpler and fast enough |
| Page with slow database queries | Yes — stream the shell while data loads |
| Pages with above-the-fold and below-the-fold content | Yes — shell renders above the fold immediately |
| API or raw response endpoints | No — use raw responses |
Server-level streaming option
Streaming is enabled by default in createServer. Disable it globally with stream: false:
createServer(specs, {
stream: false, // disable streaming for all specs
})
Even with global streaming enabled, only specs that declare a stream field will use chunked responses. All other specs use regular buffered SSR.