SalakCode SalakCode
Rendering

Streaming SSR

Progressive HTML rendering for better UX

Advanced
ssr react performance streaming

Definition

Streaming SSR sends HTML progressively from the server to the browser as it becomes available, rather than waiting for the entire page to render. This reduces Time to First Byte (TTFB) and allows the browser to start parsing and displaying content earlier.

How Streaming SSR Works

Traditional SSR:                    Streaming SSR:
                                    
Server:                            Server:
1. Render entire app               1. Render shell <html>...<head>
2. Wait for all data               2. Send immediately
3. Generate full HTML              3. Render component 1
4. Send response                   4. Stream <component-1>
                                   5. Render component 2
Client:                            6. Stream </component-1>
5. Receive HTML                    7. Continue...
6. Display page                    
7. Hydrate                         Client:
                                   2. Receive <html>...<head>
                                   3. Start parsing CSS
                                   4. Display partial content
                                   5. Receive more HTML
                                   6. Continue rendering

React 18 Streaming

renderToPipeableStream

// server.js
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';

app.get('/', (req, res) => {
  const { pipe } = renderToPipeableStream(
    <App />,
    {
      bootstrapScripts: ['/main.js'],
      onShellReady() {
        // Shell ready - start streaming
        res.statusCode = 200;
        res.setHeader('Content-type', 'text/html');
        pipe(res);
      },
      onError(error) {
        console.error(error);
        res.statusCode = 500;
      },
    }
  );
});

Streaming with Suspense

// App.js
import { Suspense } from 'react';

function App() {
  return (
    <html>
      <head>...</head>
      <body>
        <header>Static header</header>
        
        <Suspense fallback={<Spinner />}>
          <SlowComponent />  <!-- Streamed when ready -->
        </Suspense>
        
        <footer>Static footer</footer>
      </body>
    </html>
  );
}

async function SlowComponent() {
  const data = await fetchData();  // Slow API call
  return <div>{data.content}</div>;
}

Inline Fallbacks

// React streams fallback HTML first
<div id="S:1"><!--$?-->
  <div class="spinner">Loading...</div>
</div>

// Later, when data ready
<script>
  $RC=function(b,c,e){c=document.getElementById(c);c.parentNode...</script>
  <div id="S:1">
    <!-- Actual content -->
    <div>Loaded content</div>
  </div>
</template>

Benefits

1. Faster TTFB

// Traditional: Wait for all data
// TTFB: 800ms (waiting for slow API)

// Streaming: Send shell immediately  
// TTFB: 50ms (shell sent)
// Complete: 800ms (content streamed)

2. Progressive Hydration

// Browser can hydrate components as they arrive
// No need to wait for full HTML

// Interactive parts work while others loading
<Suspense>
  <InteractiveHeader />  <!-- Hydrated immediately -->
  <SlowContent />      <!-- Hydrated when streamed -->
</Suspense>

3. Better Error Handling

renderToPipeableStream(
  <App />,
  {
    onShellReady() {
      // Shell rendered successfully
      // Can start streaming
    },
    onError(error) {
      // Handle error gracefully
      // Can still stream partial content
    },
  }
);

Implementation Patterns

Selective Hydration

// Use defer for non-critical components
import { Suspense, lazy } from 'react';

const Comments = lazy(() => import('./Comments'));

function Post() {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments postId={post.id} />
      </Suspense>
    </article>
  );
}

Data Fetching

// Server-side data fetching with streaming
async function ProductPage({ id }) {
  // Start fetching immediately
  const productPromise = fetchProduct(id);
  const reviewsPromise = fetchReviews(id);
  
  return (
    <div>
      <Suspense fallback={<ProductSkeleton />}>
        <ProductDetails promise={productPromise} />
      </Suspense>
      
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews promise={reviewsPromise} />
      </Suspense>
    </div>
  );
}

async function ProductDetails({ promise }) {
  const product = await promise;
  return <ProductCard product={product} />;
}

Next.js Implementation

app Router (Streaming by Default)

// app/page.js
import { Suspense } from 'react';

export default async function Page() {
  return (
    <>
      <header>Static header</header>
      
      <Suspense fallback={<Loading />}>
        <Posts />  <!-- Automatically streamed -->
      </Suspense>
    </>
  );
}

async function Posts() {
  const posts = await getPosts();  // Slow fetch
  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  );
}

Performance Considerations

Chunking

// Break large pages into chunks
// Each Suspense boundary becomes a chunk

<Suspense>
  <Header />      <!-- Chunk 1 -->
</Suspense>

<Suspense>
  <MainContent /> <!-- Chunk 2 -->
</Suspense>

<Suspense>
  <Footer />      <!-- Chunk 3 -->
</Suspense>

Compression

// Use gzip/brotli for streaming content
app.get('/', (req, res) => {
  res.setHeader('Content-Encoding', 'gzip');
  // ... renderToPipeableStream
});

Debugging

Chrome DevTools

1. Network tab
2. Look for streaming response
3. Check Timing: TTFB vs Content Download
4. Streaming shows immediate TTFB, gradual download

Logging

const { pipe } = renderToPipeableStream(
  <App />,
  {
    onShellReady() {
      console.log('Shell ready at:', performance.now());
    },
    onAllReady() {
      console.log('All ready at:', performance.now());
    },
  }
);
Key Takeaway

Streaming SSR sends HTML progressively, improving perceived performance. Use React 18’s renderToPipeableStream, wrap slow components with Suspense, and leverage selective hydration. The shell renders immediately while dynamic content streams in, giving users faster visual feedback.

Resources

Related Topics