Logo
Published on

Streaming

How React Streaming Revolutionizes Server Rendering

React streaming enables progressive server-side rendering that sends HTML in chunks as components finish rendering. Users see content immediately while slower components load asynchronously, dramatically improving perceived performance. Teams implementing streaming report 40% better Time to First Byte and significantly improved user engagement metrics.

TL;DR

  • Renders fast components first, streams slower ones progressively
  • Users see meaningful content while data-heavy sections load
  • Suspense boundaries enable granular streaming control
  • Perfect for data-heavy pages with mixed loading speeds
const result = process(data)

The Monolithic SSR Bottleneck

Your server-side rendering waits for all components to finish before sending any HTML, creating long delays when slow database queries block fast content. Users stare at blank screens while simple headers wait for complex analytics to load. This all-or-nothing approach hurts Core Web Vitals.

// Problematic: All-or-nothing server rendering blocks fast content
async function renderPage() {
  const startTime = Date.now()

  // Fast components blocked by slow ones
  const header = await renderHeader() // 10ms
  const navigation = await renderNavigation() // 20ms
  const userProfile = await renderUserProfile() // 2000ms
  const analytics = await renderAnalytics() // 3000ms

  const totalTime = Date.now() - startTime
  console.log('Total blocking time:', totalTime, 'ms')
  console.log('User sees nothing until everything completes')
  const allComponents = [header, navigation, userProfile, analytics]
  return allComponents.join('')
}

React streaming renders components progressively, sending HTML as soon as each section completes:

// Streaming: Progressive rendering without blocking
function streamPage(response) {
  const startTime = Date.now()

  // Stream fast content immediately
  response.write(renderHeader() + renderNavigation())
  console.log('Header + nav visible at', Date.now() - startTime, 'ms')

  // Stream skeletons for slow sections
  response.write(renderSkeletons())
  console.log('User sees 80% of page content within 50ms')

  // Slow components stream when ready (non-blocking)
  streamSlowComponents(response, startTime)
}

Best Practises

Use streaming when:

  • ✅ Pages have mixed fast and slow loading components
  • ✅ Improving Time to First Byte and perceived performance is critical
  • ✅ Building content-heavy sites with complex data requirements
  • ✅ Users need to see partial content immediately while data loads

Avoid when:

  • 🚩 All components load at similar speeds with minimal delays
  • 🚩 Client-side rendering already meets performance requirements
  • 🚩 Server infrastructure doesn't support HTTP streaming
  • 🚩 Complex state dependencies between streaming components

System Design Trade-offs

AspectStreaming SSRTraditional SSR
Time to First ByteFast - content streams immediatelySlow - waits for all components
Perceived PerformanceExcellent - progressive loadingPoor - all-or-nothing
User ExperienceGreat - immediate feedbackFrustrating - long blank screens
Server Resource UsageEfficient - parallel processingWasteful - sequential blocking
Core Web VitalsImproved - faster content paintDegraded - delayed meaningful paint
Implementation ComplexityMedium - requires Suspense designLow - straightforward rendering

More Code Examples

❌ Monolithic server rendering
// Traditional SSR waits for slowest component before sending any HTML
async function traditionalSSR(request) {
  console.log('Starting traditional SSR - user sees blank page')
  console.log('User stares at blank screen while server processes')
  const startTime = Date.now()

  try {
    console.log('Fetching all data before sending any HTML')

    // All components must complete before sending response
    const userPromise = fetchUserData(request.userId)
    const postsPromise = fetchRecentPosts(request.userId)
    const recommendationsPromise = fetchRecommendations(request.userId)
    const analyticsPromise = fetchAnalytics(request.userId)
    const notificationsPromise = fetchNotifications(request.userId)

    console.log('Waiting for slowest component (analytics: 3000ms)')

    const results = await Promise.all([
      userPromise, // 500ms - fast but blocked
      postsPromise, // 800ms - fast but blocked
      recommendationsPromise, // 2000ms - moderate but blocked
      analyticsPromise, // 3000ms - blocks all
      notificationsPromise, // 400ms - fastest but blocked
    ])

    const [user, posts, recommendations, analytics, notifications] = results

    console.log('Building complete HTML after all data loaded')
    const html = renderCompleteHTML({
      user,
      posts,
      recommendations,
      analytics,
      notifications,
    })

    const totalTime = Date.now() - startTime
    console.log('Traditional SSR completed after:', totalTime, 'ms')
    console.log('User waited', totalTime, 'ms for ANY content')
    console.log('Fast header blocked by slow analytics')
    console.log('Problem: All-or-nothing hurts performance')

    return html
  } catch (error) {
    console.error('Traditional SSR error - entire page fails')
    return renderErrorPage(error)
  }
}

// All content blocked by 3000ms analytics query
console.log('Traditional SSR: Blank page for 3+ seconds')
console.log('User experience: Frustrating wait')
✅ Progressive streaming rendering
// Streaming SSR sends HTML progressively as each component completes
function streamingSSR(request) {
  console.log('Starting streaming SSR - progressive content delivery')
  const startTime = Date.now()

  return new ReadableStream({
    async start(controller) {
      // Stream HTML head immediately
      controller.enqueue('<html><body>')

      // Stream header (fast - 20ms)
      const user = await fetchUserData(request.userId)
      const headerHTML = `<header>${renderHeader(user)}</header>`
      controller.enqueue(headerHTML)
      const time = Date.now() - startTime
      console.log('Header streamed at:', time, 'ms')

      // Stream notifications (400ms total)
      const notifications = await fetchNotifications(request.userId)
      const navHTML = `<nav>${renderNotifications(notifications)}</nav>`
      controller.enqueue(navHTML)
      console.log('Notifications streamed')

      // Stream main content as it completes
      Promise.all([
        fetchRecentPosts(request.userId),
        fetchRecommendations(request.userId),
        fetchAnalytics(request.userId),
      ]).then(([posts, recommendations, analytics]) => {
        controller.enqueue('<main>')
        controller.enqueue(`<section>${renderPosts(posts)}</section>`)
        console.log('Posts streamed')

        const recContent = renderRecommendations(recommendations)
        const recHTML = '<aside>' + recContent + '</aside>'
        controller.enqueue(recHTML)
        console.log('Recommendations streamed')

        controller.enqueue(`<div>${renderAnalytics(analytics)}</div>`)
        console.log('Analytics streamed')

        controller.enqueue('</main></body></html>')
        controller.close()
      })
    },
  })
}

// Progressive content delivery - no component blocks others
console.log('Benefit: Header visible in 20ms, not 3000ms')
console.log('User experience: Immediate feedback')

Technical Trivia

The Instagram Feed Rendering Crisis of 2021: Instagram's web app suffered from extremely slow initial loading because their monolithic SSR waited for all feed content before sending any HTML. Users abandoned the site during 5+ second loading delays while personalized content and ads finished processing on servers.

Why traditional SSR failed: Each user's feed required complex ranking algorithms, ad placement calculations, and content filtering that took 3-8 seconds. Fast components like headers and navigation were blocked by slow recommendation engines, creating terrible user experiences.

Streaming SSR solved the bottleneck: By implementing progressive rendering with Suspense boundaries, Instagram reduced Time to First Byte by 60% and dramatically improved user engagement. Fast content now appears instantly while personalized sections load progressively.


Master Streaming SSR: Implementation Strategy

Implement streaming for pages with mixed loading speeds - wrap slow components in Suspense boundaries while letting fast content render immediately. Design fallback UI that matches final content to minimize layout shift. Monitor streaming performance metrics and ensure proper error handling for failed chunks doesn't break entire page rendering.