React streaming explained: The future of web rendering - Featured Image
Web development10 min read

React streaming explained: The future of web rendering

React streaming is a different way web apps deliver content to users. It changes how we think about page loading and user experience. If you're building a site with lots of content or a dynamic app, streaming can make your app feel faster.

Streaming is basically sending audio or video content over the internet in real-time. Users can watch or listen without downloading the whole file first. It works by sending data in small pieces that play back as they arrive.

What is React streaming?

React streaming is a modern rendering technique that fundamentally changes how web applications deliver content to users. Let's break down what it means and how it works. React streaming is a way to render your app that lets you send HTML content to the browser bit by bit, even before all the data is ready. Instead of making users wait for everything to load before showing anything, streaming breaks things into chunks that get sent as soon as they're ready.

Technically speaking, React streaming uses two main ideas:

  1. Server Components: React components that run on the server

  2. Suspense: A React feature that lets you "wait" for code or data while showing a loading state

Together, these let parts of your UI show up as they become ready, instead of everything appearing at once.

Why use React streaming?

React streaming offers several key advantages that make your web applications faster and more user-friendly. Here are the main reasons developers choose streaming over traditional rendering methods.

1. Improved perceived performance

The biggest benefit is that your app feels faster. Users see content appear gradually instead of staring at a blank screen:

  • Faster Time to First Byte (TTFB) - Initial HTML shows up quicker

  • Progressive rendering - Content appears in stages as it's ready

  • Fewer loading spinners - Parts of the UI are visible while other parts load

2. Better user experience

Streaming creates a smoother, less jarring experience:

  • Content appears bit by bit rather than all at once

  • Important UI parts can load first

  • Users can start using parts of the page while other sections load

3. SEO benefits

Search engines can see your content sooner, which helps with SEO:

  • Faster content delivery means crawlers get useful content quicker

  • Better Core Web Vitals scores, especially Largest Contentful Paint (LCP)

Official position vs reality

Next.js docs say that "since streaming is server-rendered, it doesn't impact SEO" and Vercel confirms "streaming helps improve user experience without affecting SEO and streamed content can still be indexed by Google."

But here's what you should keep in mind:

Critical content first: Make sure important SEO stuff (title, meta tags, main content) renders early in the stream.

Reasonable stream duration: Keep total streaming time reasonable. If components take too long, consider showing static fallbacks instead of making crawlers wait.

Core Web Vitals: Streaming can make things feel faster but might not always improve actual Core Web Vitals if not done carefully. Poor CWV scores hurt SEO.

Fallback strategy: For important SEO pages, consider having non-streamed backups or make sure essential content doesn't depend on slow-loading parts.

4. Better resource utilization

Streaming uses both server and client resources more efficiently:

  • Server resources get freed up sooner as responses are sent in chunks

  • Client processing is spread out rather than happening all at once

How React streaming works

React streaming works by combining several modern web technologies and patterns to deliver content progressively. Here's how these pieces fit together to create a seamless user experience.

React streaming combines several technologies and patterns:

Server components

React Server Components (RSC) are components that run entirely on the server. They:

  • Can access server-only resources like databases directly

  • Don't include any client-side JavaScript in the bundle

  • Return HTML and references to Client Components

const movies = await getMovies()

Suspense boundaries

Suspense is a React component that lets you specify what to show while content is loading:

<Suspense fallback={<LoadingSpinner />}>
  <ComponentThatFetchesData />
</Suspense>

With streaming, when a component suspends (waits for data), the server can:

  • Send the fallback UI immediately

  • Continue rendering other parts of the page

  • Send the actual component HTML when data is ready

The streaming process

  1. Initial request: Browser asks for a page

  2. Fast initial response: Server quickly sends HTML for the page shell and any ready components

  3. Progressive updates: As more data becomes available, server sends additional HTML chunks

  4. Hydration: Client-side JavaScript adds interactivity to the streamed HTML

Implementing React streaming

Ready to add streaming to your React application? There are multiple ways to implement React streaming depending on your setup and requirements. Here are the most popular approaches.

Note: React streaming works great when combined with other Next.js techniques like Static Site Generation (SSG), Incremental Static Regeneration (ISR), and partial prerendering. These approaches work well with streaming by providing static content that can be sent immediately, while dynamic content streams in afterward. This gives you the best of both worlds: instant loading of static parts and progressive loading of dynamic content.

Using Next.js App Router

Next.js (version 13+) makes React streaming simple with its App Router:

// app/page.js
import { Suspense } from 'react';
import Profile from './profile';
import Weather from './weather';
import Posts from './posts';

export default function HomePage() {
  return (
    <div>
      <h1>Welcome to My App</h1>
      
      {/* This loads immediately */}
      <section>
        <h2>Static Content</h2>
        <p>This content is always available immediately.</p>
      </section>
      
      {/* These stream in as they become ready */}
      <section>
        <h2>Dynamic Content</h2>
        
        <div className="profile-section">
          <Suspense fallback={<p>Loading profile...</p>}>
            <Profile />
          </Suspense>
        </div>
        
        <div className="weather-section">
          <Suspense fallback={<p>Loading weather...</p>}>
            <Weather />
          </Suspense>
        </div>
        
        <div className="posts-grid">
          <Suspense fallback={<p>Loading posts...</p>}>
            <Posts />
          </Suspense>
        </div>
      </section>
    </div>
  );
}

The Profile, Weather, and Posts components can fetch their own data:

// app/posts.js
async function getPosts() {
  // Simulate a slow API call
  const res = await fetch('https://jsonplaceholder.typicode.com/posts');
  await new Promise(resolve => setTimeout(resolve, 2000)); // Artificial delay
  return res.json();
}

export default async function Posts() {
  const posts = await getPosts();
  
  return (
    <ul>
      {posts.slice(0, 5).map(post => (
        <li key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </li>
      ))}
    </ul>
  );
}

Using React 18+ with Express

If you're not using Next.js, you can implement streaming with React 18's built-in functions and a server like Express:

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

const app = express();

app.get('/', (req, res) => {
  // Set appropriate headers
  res.setHeader('Content-Type', 'text/html');
  res.setHeader('Transfer-Encoding', 'chunked');
  
  const { pipe } = renderToPipeableStream(<App />, {
    bootstrapScripts: ['/client.js'],
    onShellReady() {
      // The "shell" is ready when top-level components not wrapped in Suspense are ready
      pipe(res);
    },
    onAllReady() {
      // Optionally, you can do something when everything is ready
      // But with streaming, we don't wait for this
    }
  });
});

app.listen(3000);

Some streaming patterns

There are several proven patterns for implementing React streaming effectively. These patterns help you structure your application for optimal performance and user experience.

Progressive loading sequences

Smart placement of Suspense boundaries can create a nice loading sequence:

<Layout>
  {/* Header loads first */}
  <Header />
  
  <Suspense fallback={<MainSkeleton />}>
    {/* Main content loads second */}
    <MainContent />
    
    {/* Related items load last */}
    <Suspense fallback={<RelatedItemsSkeleton />}>
      <RelatedItems />
    </Suspense>
  </Suspense>
  
  {/* Footer loads with the initial shell */}
  <Footer />
</Layout>

Streaming lists with pagination

For long lists, you can stream items as they become available:

function ItemsList() {
  return (
    <ul>
      {Array.from({ length: 100 }).map((_, i) => (
        <Suspense key={i} fallback={<ItemSkeleton />}>
          <Item itemId={i} />
        </Suspense>
      ))}
    </ul>
  );
}

// Each Item component can fetch its own data
async function Item({ itemId }) {
  const data = await fetchItemData(itemId);
  return <li>{data.title}</li>;
}

Parallel vs sequential loading

With streaming, you can choose between parallel and sequential loading patterns:

// Parallel loading - all requests start at the same time
<div>
  <Suspense fallback={<Spinner />}><ComponentA /></Suspense>
  <Suspense fallback={<Spinner />}><ComponentB /></Suspense>
  <Suspense fallback={<Spinner />}><ComponentC /></Suspense>
</div>

// Sequential loading - ComponentB only starts loading after ComponentA is done
<div>
  <Suspense fallback={<Spinner />}>
    <ComponentA />
    <ComponentB />
    <ComponentC />
  </Suspense>
</div>

Common challenges and solutions

While React streaming offers many benefits, you might encounter some challenges during implementation. Here are the most common issues and their practical solutions.

1. Layout shifts

Problem: Content appearing dynamically can cause layout shifts.

Solution: Use skeleton screens with the same dimensions as the actual content:

<Suspense 
  fallback={
    <div className="skeleton" style={{ height: '200px', width: '100%' }}>
      Loading...
    </div>
  }
>
  <DynamicContent />
</Suspense>

2. Data waterfalls

Problem: Nested Suspense boundaries can create "waterfalls" where child components wait for parent components.

Solution: Use parallel data fetching techniques:

// Fetch data in parallel at the parent level
async function ParentComponent() {
  // Start both fetches immediately
  const profilePromise = fetchProfileData();
  const postsPromise = fetchPosts();
  
  // Wait for both to complete
  const [profile, posts] = await Promise.all([profilePromise, postsPromise]);
  
  return (
    <>
      <ProfileComponent profile={profile} />
      <PostsComponent posts={posts} />
    </>
  );
}

3. Hydration errors

Problem: Differences between server and client rendering can cause hydration errors.

Solution: Make sure rendering conditions are consistent and use client-side only features carefully:

Note: Remember that if a parent component is marked as client-side (using the 'use client' directive in Next.js), all of its child components will automatically be client-side too, regardless of whether they're defined as server components. This "client boundary" can't be crossed back to server rendering. Always structure your component tree to keep server components at higher levels when possible, and be smart about where you introduce client components.

// Use the useEffect hook for client-side-only code
import { useEffect, useState } from 'react';

function ClientComponent() {
  const [windowWidth, setWindowWidth] = useState(null);
  
  useEffect(() => {
    // This runs only in the browser, not during SSR
    setWindowWidth(window.innerWidth);
    
    const handleResize = () => setWindowWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  return <div>Window width: {windowWidth ?? 'Loading...'}</div>;
}

Best practices for React streaming

Following these best practices will help you get the most out of React streaming while avoiding common pitfalls.

  • Start with a solid shell - Include navigation, layout, and important UI elements in the initial response

  • Prioritize above-the-fold content - Stream visible content first

  • Use appropriate fallbacks - Skeleton screens are better than spinners

  • Consider loading sequence - Think about what order components should appear

  • Be mindful of data dependencies - Avoid unnecessary waterfalls

  • Test performance - Use tools like Lighthouse to measure improvements

When not to use streaming

React streaming isn't the perfect solution for every situation. Here are scenarios where traditional rendering might be a better choice.

Streaming isn't always the best solution:

  • Small, simple applications might not see big benefits

  • Applications where components really depend on each other

  • When SEO isn't a concern (though streaming often helps with SEO anyway)

Tools and resources

These tools and resources will help you implement, test, and optimize React streaming in your applications.

Libraries and frameworks

Next.js App Router: Built-in support for React Server Components and streaming (Next.js Documentation)

Testing and debugging

  • React Developer Tools: Has features to help you see Suspense boundaries (Chrome Extension)

  • Chrome DevTools: Network tab can show streaming chunks

  • Lighthouse: Can measure performance improvements (Lighthouse Documentation)

Conclusion

The best streaming implementations are those that really think about what users need, putting the most important content first and creating a natural, smooth loading experience.

Posted on: 29/6/2025

hassaankhan

Frontend Developer — UI/UX Enthusiast and building scalable web apps

Posted by





Subscribe to our newsletter

Join 2,000+ subscribers

Stay in the loop with everything you need to know.

We care about your data in our privacy policy

Background shadow leftBackground shadow right

Have something to share?

Write on the platform and dummy copy content

Be Part of Something Big

Shifters, a developer-first community platform, is launching soon with all the features. Don't miss out on day one access. Join the waitlist: