We use cookies to enhance your experience on the site
CodeWorlds

Data Streaming and React Suspense

In modern web application development, performance and user interface responsiveness are key factors affecting user experience. Next.js, as a frontend framework, introduces advanced techniques that help achieve a smooth user experience even with complex data fetching operations. In this module, we will look at how Next.js App Router uses data streaming and React Suspense to create more responsive applications.

Problems with Traditional Rendering

Before moving on to data streaming, it's worth understanding the limitations of traditional rendering approaches:

  1. Blocking rendering - classically, the entire page must wait until all data is fetched before anything is displayed to the user
  2. Waterfall data fetching - sequential data fetching where each request must wait for the previous one to complete
  3. All or nothing - the entire page is rendered as one indivisible whole

These limitations lead to slower Time To First Byte (TTFB) and can cause user frustration, especially on slower connections or with complex database queries.

Data Streaming - A New Approach

Data streaming, introduced in the Next.js App Router, allows for progressively sending UI fragments from the server to the client as they become ready, instead of waiting for all data fetching operations to complete.

Key advantages of this approach include:

  1. Faster TTFB (Time To First Byte) - the user sees the first content sooner
  2. Progressive rendering - the UI is rendered gradually, giving a sense of faster responsiveness
  3. Avoiding blocking - slower components don't block the rendering of faster components

React Suspense - The Foundation of Streaming in React

Suspense is a React feature introduced in version 16.6 that allows "suspending" component rendering until a certain condition is met (e.g., data has loaded). Next.js App Router fully leverages this functionality to implement data streaming.

The fundamental idea of Suspense is:

  1. Allowing components to "pause" rendering while waiting for data
  2. Displaying a fallback component during loading
  3. Replacing the fallback with the actual component when the data is ready

Implementing Data Streaming in Next.js App Router

1. Basic Example of Using React Suspense

Let's start with a basic example showing how to use Suspense in Next.js:

1// app/dashboard/page.tsx
2import { Suspense } from 'react';
3import { UserProfile } from './user-profile';
4import { RecentOrders } from './recent-orders';
5import { Analytics } from './analytics';
6import { Loading } from '@/components/loading';
7
8export default function DashboardPage() {
9  return (
10    <div className="dashboard-container">
11      <h1>User Dashboard</h1>
12
13      {/* Immediately rendered header */}
14      <div className="dashboard-header">
15        <Suspense fallback={<Loading />}>
16          <UserProfile />
17        </Suspense>
18      </div>
19
20      <div className="dashboard-grid">
21        {/* Orders section with its own loading state */}
22        <div className="orders-section">
23          <Suspense fallback={<Loading type="orders" />}>
24            <RecentOrders />
25          </Suspense>
26        </div>
27
28        {/* Analytics section with its own loading state */}
29        <div className="analytics-section">
30          <Suspense fallback={<Loading type="analytics" />}>
31            <Analytics />
32          </Suspense>
33        </div>
34      </div>
35    </div>
36  );
37}

In this example:

  1. The dashboard page is divided into independent sections
  2. Each section is wrapped in a
    Suspense
    component with an appropriate fallback
  3. Sections are rendered independently when their data is ready

The

UserProfile
,
RecentOrders
, and
Analytics
components can asynchronously fetch data:

1// app/dashboard/user-profile.tsx
2async function getUserData() {
3  // Fetching user data - may take 500ms
4  const res = await fetch('/api/user');
5  return res.json();
6}
7
8export async function UserProfile() {
9  const user = await getUserData();
10
11  return (
12    <div className="user-profile">
13      <img src={user.avatar} alt={user.name} />
14      <h2>{user.name}</h2>
15      <p>{user.email}</p>
16    </div>
17  );
18}
1// app/dashboard/recent-orders.tsx
2async function getRecentOrders() {
3  // Fetching recent orders - may take 2s
4  const res = await fetch('/api/orders/recent');
5  return res.json();
6}
7
8export async function RecentOrders() {
9  const orders = await getRecentOrders();
10
11  return (
12    <div>
13      <h3>Recent Orders</h3>
14      <table>
15        <thead>
16          <tr>
17            <th>ID</th>
18            <th>Product</th>
19            <th>Price</th>
20            <th>Status</th>
21          </tr>
22        </thead>
23        <tbody>
24          {orders.map(order => (
25            <tr key={order.id}>
26              <td>{order.id}</td>
27              <td>{order.product}</td>
28              <td>${order.price}</td>
29              <td>{order.status}</td>
30            </tr>
31          ))}
32        </tbody>
33      </table>
34    </div>
35  );
36}

2. Nested Suspense Components

We can create more complex structures with nested Suspense components:

1// app/products/[category]/page.tsx
2import { Suspense } from 'react';
3import { CategoryHeader } from './category-header';
4import { ProductGrid } from './product-grid';
5import { ProductFilter } from './product-filter';
6import { RelatedCategories } from './related-categories';
7import { SimilarProducts } from './similar-products';
8import { Loading } from '@/components/loading';
9
10export default async function CategoryPage({ params }) {
11  return (
12    <div className="category-page">
13      {/* Nested Suspense components */}
14      <Suspense fallback={<Loading height="200px" />}>
15        <CategoryHeader category={(await params).category} />
16
17        <div className="product-layout">
18          <aside className="filters">
19            <Suspense fallback={<Loading type="filters" />}>
20              <ProductFilter category={(await params).category} />
21            </Suspense>
22          </aside>
23
24          <main className="products">
25            <Suspense fallback={<Loading type="products" count={12} />}>
26              <ProductGrid category={(await params).category} />
27            </Suspense>
28          </main>
29        </div>
30
31        <div className="recommendations">
32          <Suspense fallback={<Loading type="categories" />}>
33            <RelatedCategories category={(await params).category} />
34          </Suspense>
35
36          <Suspense fallback={<Loading type="products" />}>
37            <SimilarProducts category={(await params).category} />
38          </Suspense>
39        </div>
40      </Suspense>
41    </div>
42  );
43}

In this example:

  1. The outer
    Suspense
    wraps the entire category view
  2. Inner
    Suspense
    components split the page into smaller, independently loading sections
  3. Each section has its own loading state tailored to the content type

3. Streaming with Client Components

We can combine streaming with client components, giving us the ability to create interactive UIs with smooth loading:

1// app/products/[id]/page.tsx
2import { Suspense } from 'react';
3import { ProductDetails } from './product-details';
4import { ProductReviews } from './product-reviews';
5import AddToCartButton from './add-to-cart-button'; // Client component
6import ProductImageGallery from './product-image-gallery'; // Client component
7import { Loading } from '@/components/loading';
8
9export default async function ProductPage({ params }) {
10  return (
11    <div className="product-page">
12      <div className="product-main">
13        <Suspense fallback={<Loading type="image-gallery" />}>
14          <ProductImageWrapper productId={(await params).id} />
15        </Suspense>
16
17        <div className="product-info">
18          <Suspense fallback={<Loading type="product-details" />}>
19            <ProductDetailsWrapper productId={(await params).id} />
20          </Suspense>
21
22          {/* Client component for interactivity */}
23          <AddToCartButton productId={(await params).id} />
24        </div>
25      </div>
26
27      <div className="product-reviews">
28        <Suspense fallback={<Loading type="reviews" />}>
29          <ProductReviews productId={(await params).id} />
30        </Suspense>
31      </div>
32    </div>
33  );
34}
35
36// Wrapper components that need props
37async function ProductImageWrapper({ productId }) {
38  const product = await getProduct(productId);
39  return <ProductImageGallery images={product.images} />;
40}
41
42async function ProductDetailsWrapper({ productId }) {
43  const product = await getProduct(productId);
44  return <ProductDetails product={product} />;
45}
46
47async function getProduct(id) {
48  const res = await fetch(`/api/products/${id}`);
49  return res.json();
50}

In this example:

  1. ProductImageGallery
    and
    AddToCartButton
    are client components (
    'use client'
    )
  2. We wrap client components in asynchronous server components to pass them data
  3. The
    AddToCartButton
    component is interactive immediately after loading, even when other parts of the page are still loading

Advanced Data Streaming Techniques

1. Parallel Data Fetching

One of the best practices for data streaming is parallelizing requests to avoid waterfall data fetching:

1// app/dashboard/page.tsx
2import { Suspense } from 'react';
3import { UserProfile } from './user-profile';
4import { RecentOrders } from './recent-orders';
5import { Analytics } from './analytics';
6import { Loading } from '@/components/loading';
7
8// Pre-initializing queries - avoiding the data waterfall
9export function generateMetadata() {
10  // We initialize data fetching early so it's ready
11  // when components need it
12  preloadUserData();
13  preloadRecentOrders();
14  preloadAnalytics();
15
16  return {
17    title: 'Dashboard - My Application',
18  };
19}
20
21// Pre-initialization functions
22function preloadUserData() {
23  void getUserData(); // void ignores the Promise
24}
25
26function preloadRecentOrders() {
27  void getRecentOrders(); // void ignores the Promise
28}
29
30function preloadAnalytics() {
31  void getAnalyticsData(); // void ignores the Promise
32}
33
34// Data fetching functions with cache
35async function getUserData() {
36  const res = await fetch('/api/user', { cache: 'force-cache' });
37  return res.json();
38}
39
40async function getRecentOrders() {
41  const res = await fetch('/api/orders/recent', { cache: 'force-cache' });
42  return res.json();
43}
44
45async function getAnalyticsData() {
46  const res = await fetch('/api/analytics', { cache: 'force-cache' });
47  return res.json();
48}
49
50export default function DashboardPage() {
51  return (
52    <div className="dashboard-container">
53      {/* ... page structure with Suspense components, as before ... */}
54    </div>
55  );
56}

In this approach:

  1. We initialize data fetching already in the
    generateMetadata
    function, before components request it
  2. We use
    void
    to ignore the Promise, since we don't need its result in this function
  3. In components, we use the same data fetching functions that utilize the cache

2. Cascading Suspense - Content Prioritization

We can strategically prioritize content loading so the most important elements are displayed first:

1// app/article/[slug]/page.tsx
2import { Suspense } from 'react';
3import { ArticleHeader } from './article-header';
4import { ArticleContent } from './article-content';
5import { ArticleComments } from './article-comments';
6import { RelatedArticles } from './related-articles';
7import { AuthorBio } from './author-bio';
8import { SocialSharing } from './social-sharing';
9import { Loading } from '@/components/loading';
10
11export default async function ArticlePage({ params }) {
12  return (
13    <article className="article-container">
14      {/* Priority 1: Article header - loads first */}
15      <Suspense fallback={<Loading type="header" />}>
16        <ArticleHeader slug={(await params).slug} />
17      </Suspense>
18
19      <div className="article-grid">
20        <div className="article-main">
21          {/* Priority 2: Article content */}
22          <Suspense fallback={<Loading type="content" />}>
23            <ArticleContent slug={(await params).slug} />
24
25            <div className="article-footer">
26              {/* Priority 3: Author and sharing - loads after content */}
27              <Suspense fallback={<Loading type="author" />}>
28                <AuthorBio slug={(await params).slug} />
29              </Suspense>
30
31              <SocialSharing url={`/article/${(await params).slug}`} />
32            </div>
33          </Suspense>
34        </div>
35
36        <aside className="article-sidebar">
37          {/* Priority 4: Related articles - loads at the end */}
38          <Suspense fallback={<Loading type="related" />}>
39            <RelatedArticles slug={(await params).slug} />
40          </Suspense>
41        </aside>
42      </div>
43
44      {/* Priority 5: Comments - loads last */}
45      <Suspense fallback={<Loading type="comments" />}>
46        <ArticleComments slug={(await params).slug} />
47      </Suspense>
48    </article>
49  );
50}

In this example:

  1. We establish a clear priority hierarchy for page content
  2. The most critical elements (header, content) are streamed first
  3. Less critical elements (comments, related articles) are streamed later
  4. By nesting Suspense components, we create a natural content loading flow

3. Streaming Components with Dependent Data

Sometimes we need one component to wait for data from another component. We can achieve this using context and shared resources:

1// app/shop/[category]/[subcategory]/page.tsx
2import { Suspense } from 'react';
3import { CategoryProvider } from './category-context';
4import { CategoryInfo } from './category-info';
5import { SubcategoryProducts } from './subcategory-products';
6import { Loading } from '@/components/loading';
7
8export default async function SubcategoryPage({ params }) {
9  const { category, subcategory } = await params;
10
11  return (
12    <div className="shop-page">
13      <CategoryProvider category={category}>
14        <Suspense fallback={<Loading type="category" />}>
15          <CategoryInfo />
16
17          {/* This component needs data from CategoryInfo */}
18          <Suspense fallback={<Loading type="products" />}>
19            <SubcategoryProducts subcategory={subcategory} />
20          </Suspense>
21        </Suspense>
22      </CategoryProvider>
23    </div>
24  );
25}
1// app/shop/[category]/[subcategory]/category-context.tsx
2'use client';
3
4import { createContext, useContext, useState, useEffect } from 'react';
5
6const CategoryContext = createContext(null);
7
8export function CategoryProvider({ children, category }) {
9  const [categoryData, setCategoryData] = useState(null);
10  const [loading, setLoading] = useState(true);
11
12  useEffect(() => {
13    async function loadCategoryData() {
14      try {
15        const res = await fetch(`/api/categories/${category}`);
16        const data = await res.json();
17        setCategoryData(data);
18      } catch (error) {
19        console.error('Error loading category data:', error);
20      } finally {
21        setLoading(false);
22      }
23    }
24
25    loadCategoryData();
26  }, [category]);
27
28  return (
29    <CategoryContext.Provider value={{ categoryData, loading }}>
30      {children}
31    </CategoryContext.Provider>
32  );
33}
34
35export function useCategoryContext() {
36  const context = useContext(CategoryContext);
37  if (!context) {
38    throw new Error('useCategoryContext must be used within a CategoryProvider');
39  }
40  return context;
41}

In the components receiving data:

1// app/shop/[category]/[subcategory]/category-info.tsx
2'use client';
3
4import { useCategoryContext } from './category-context';
5
6export function CategoryInfo() {
7  const { categoryData, loading } = useCategoryContext();
8
9  if (loading) {
10    return <div>Loading category information...</div>;
11  }
12
13  return (
14    <div className="category-info">
15      <h1>{categoryData.name}</h1>
16      <p>{categoryData.description}</p>
17      <div className="category-stats">
18        <span>{categoryData.productCount} products</span>
19        <span>{categoryData.brandsCount} brands</span>
20      </div>
21    </div>
22  );
23}
1// app/shop/[category]/[subcategory]/subcategory-products.tsx
2'use client';
3
4import { useCategoryContext } from './category-context';
5import { useEffect, useState } from 'react';
6
7export function SubcategoryProducts({ subcategory }) {
8  const { categoryData } = useCategoryContext();
9  const [products, setProducts] = useState([]);
10  const [loading, setLoading] = useState(true);
11
12  useEffect(() => {
13    // Wait until categoryData is available
14    if (!categoryData) return;
15
16    async function loadProducts() {
17      try {
18        // Use categoryData to fetch products
19        const res = await fetch(
20          `/api/products?category=${categoryData.id}&subcategory=${subcategory}`
21        );
22        const data = await res.json();
23        setProducts(data);
24      } catch (error) {
25        console.error('Error loading products:', error);
26      } finally {
27        setLoading(false);
28      }
29    }
30
31    loadProducts();
32  }, [categoryData, subcategory]);
33
34  if (loading) {
35    return <div>Loading products...</div>;
36  }
37
38  return (
39    <div className="products-grid">
40      {products.map(product => (
41        <div key={product.id} className="product-card">
42          <img src={product.image} alt={product.name} />
43          <h3>{product.name}</h3>
44          <p>${product.price}</p>
45        </div>
46      ))}
47    </div>
48  );
49}

Server Components vs. Client Components in the Context of Streaming

When implementing data streaming in Next.js App Router, it's important to understand the differences between server and client components:

Server Components

Server components are ideal for streaming because:

  1. They can asynchronously fetch data directly from databases or APIs
  2. They are rendered on the server and sent to the client as HTML
  3. They can be wrapped in
    Suspense
    for loading state handling
1// app/products/product-list.tsx (Server Component)
2async function getProducts() {
3  // Direct database access on the server
4  const products = await db.products.findMany();
5  return products;
6}
7
8export async function ProductList() {
9  const products = await getProducts();
10
11  return (
12    <div className="product-list">
13      {products.map(product => (
14        <div key={product.id} className="product-item">
15          <h3>{product.name}</h3>
16          <p>${product.price}</p>
17        </div>
18      ))}
19    </div>
20  );
21}

Client Components

Client components are needed for interactivity but have different behavior in the context of streaming:

  1. They cannot be directly asynchronous
  2. They use React hooks (useState, useEffect) to fetch data
  3. They are rendered on the client and require sending JS to the browser
1// app/products/filter-panel.tsx (Client Component)
2'use client';
3
4import { useState, useEffect } from 'react';
5
6export function FilterPanel({ onFilterChange }) {
7  const [categories, setCategories] = useState([]);
8  const [loading, setLoading] = useState(true);
9  const [selectedCategory, setSelectedCategory] = useState(null);
10
11  useEffect(() => {
12    async function loadCategories() {
13      try {
14        const res = await fetch('/api/categories');
15        const data = await res.json();
16        setCategories(data);
17      } catch (error) {
18        console.error('Error loading categories:', error);
19      } finally {
20        setLoading(false);
21      }
22    }
23
24    loadCategories();
25  }, []);
26
27  function handleCategoryChange(categoryId) {
28    setSelectedCategory(categoryId);
29    onFilterChange({ category: categoryId });
30  }
31
32  if (loading) return <div>Loading filters...</div>;
33
34  return (
35    <div className="filter-panel">
36      <h3>Categories</h3>
37      <ul>
38        {categories.map(category => (
39          <li key={category.id}>
40            <button
41              onClick={() => handleCategoryChange(category.id)}
42              className={selectedCategory === category.id ? 'active' : ''}
43            >
44              {category.name}
45            </button>
46          </li>
47        ))}
48      </ul>
49    </div>
50  );
51}

Combining Server and Client Components

The best practice is to use Server Components for fetching data and preparing content, and Client Components for interactivity:

1// app/products/page.tsx
2import { Suspense } from 'react';
3import { ProductList } from './product-list'; // Server Component
4import FilterPanel from './filter-panel'; // Client Component
5import { Loading } from '@/components/loading';
6
7export default function ProductsPage() {
8  return (
9    <div className="products-page">
10      <div className="sidebar">
11        <FilterPanel onFilterChange={/* client-side handling */} />
12      </div>
13
14      <div className="main-content">
15        <Suspense fallback={<Loading type="products" />}>
16          <ProductList />
17        </Suspense>
18      </div>
19    </div>
20  );
21}

Error Handling in Data Streaming

During data streaming, proper error handling is essential. Next.js App Router provides an

Error Boundary
component for handling errors in streamed components:

1// app/dashboard/error.tsx
2'use client';
3
4import { useEffect } from 'react';
5
6export default function Error({
7  error,
8  reset
9}: {
10  error: Error & { digest?: string };
11  reset: () => void;
12}) {
13  useEffect(() => {
14    // Log the error to a monitoring service
15    console.error('Dashboard error:', error);
16  }, [error]);
17
18  return (
19    <div className="error-container">
20      <h2>Something went wrong!</h2>
21      <p>{error.message || 'An unexpected error occurred.'}</p>
22      <button
23        onClick={() => reset()}
24        className="button"
25      >
26        Try again
27      </button>
28    </div>
29  );
30}

We can also create an Error Boundary for specific sections:

1// app/dashboard/analytics-error.tsx
2'use client';
3
4export default function AnalyticsError({ error, reset }) {
5  return (
6    <div className="section-error">
7      <h3>Error loading analytics</h3>
8      <p>We could not load the analytics data: {error.message}</p>
9      <button onClick={reset}>Try again</button>
10    </div>
11  );
12}

And then use it for specific sections:

1// app/dashboard/page.tsx
2import { Suspense } from 'react';
3import { ErrorBoundary } from 'react-error-boundary';
4import { UserProfile } from './user-profile';
5import { RecentOrders } from './recent-orders';
6import { Analytics } from './analytics';
7import AnalyticsError from './analytics-error';
8import { Loading } from '@/components/loading';
9
10export default function DashboardPage() {
11  return (
12    <div className="dashboard-container">
13      {/* ... other sections ... */}
14
15      <div className="analytics-section">
16        <ErrorBoundary FallbackComponent={AnalyticsError}>
17          <Suspense fallback={<Loading type="analytics" />}>
18            <Analytics />
19          </Suspense>
20        </ErrorBoundary>
21      </div>
22    </div>
23  );
24}

Streaming Performance Optimization

Proper implementation of data streaming can significantly improve application performance. Here are some additional optimization techniques:

1. Preventing Interface Flickering

1// app/layout.tsx
2export default function RootLayout({ children }) {
3  return (
4    <html lang="en">
5      <body>
6        {/* Setting minimum delay for Suspense */}
7        <script dangerouslySetInnerHTML={{ __html: `
8          window.SuspenseMinDelay = 300; // ms
9        ` }} />
10        {children}
11      </body>
12    </html>
13  );
14}

We can also create a custom component for delaying the loading state:

1// components/delayed-suspense.tsx
2'use client';
3
4import { Suspense, useState, useEffect } from 'react';
5
6export function DelayedSuspense({
7  children,
8  fallback,
9  delay = 300
10}) {
11  const [showFallback, setShowFallback] = useState(false);
12
13  useEffect(() => {
14    const timer = setTimeout(() => {
15      setShowFallback(true);
16    }, delay);
17
18    return () => clearTimeout(timer);
19  }, [delay]);
20
21  return (
22    <Suspense fallback={showFallback ? fallback : null}>
23      {children}
24    </Suspense>
25  );
26}

2. Prioritizing Critical Data

1// app/products/[id]/page.tsx
2import { Suspense } from 'react';
3import { ProductBasicInfo } from './product-basic-info'; // Critical data
4import { ProductDetails } from './product-details'; // Important data
5import { ProductReviews } from './product-reviews'; // Less critical data
6import { RelatedProducts } from './related-products'; // Least critical data
7import { Loading } from '@/components/loading';
8
9export default async function ProductPage({ params }) {
10  // Pre-initialize critical data
11  preloadProductBasicInfo((await params).id);
12
13  return (
14    <div className="product-page">
15      {/* Immediate rendering of critical information */}
16      <div className="product-hero">
17        <Suspense fallback={<Loading priority="high" />}>
18          <ProductBasicInfo productId={(await params).id} />
19        </Suspense>
20      </div>
21
22      {/* Less critical sections loaded later */}
23      <div className="product-content">
24        <Suspense fallback={<Loading priority="medium" />}>
25          <ProductDetails productId={(await params).id} />
26        </Suspense>
27      </div>
28
29      <div className="product-secondary">
30        <Suspense fallback={<Loading priority="low" />}>
31          <ProductReviews productId={(await params).id} />
32        </Suspense>
33      </div>
34
35      <div className="product-related">
36        <Suspense fallback={<Loading priority="lowest" />}>
37          <RelatedProducts productId={(await params).id} />
38        </Suspense>
39      </div>
40    </div>
41  );
42}
43
44// Pre-initialize critical data
45function preloadProductBasicInfo(id) {
46  void getProductBasicInfo(id);
47}
48
49async function getProductBasicInfo(id) {
50  const res = await fetch(`/api/products/${id}/basic`, {
51    priority: 'high' // Setting the fetch API priority
52  });
53  return res.json();
54}

3. Limiting Layout Shifts (CLS)

1// components/loading.tsx
2export function Loading({ type, priority = 'medium', height, width }) {
3  // Determine skeleton dimensions based on content type
4  const dimensions = getDimensionsForType(type, { height, width });
5
6  return (
7    <div
8      className={`skeleton-loader ${type} priority-${priority}`}
9      style={{
10        height: dimensions.height,
11        width: dimensions.width,
12        // Predefined dimensions prevent layout shifts
13      }}
14    >
15      <div className="skeleton-animation" />
16    </div>
17  );
18}
19
20function getDimensionsForType(type, userDimensions) {
21  // Default dimensions for different content types
22  const defaults = {
23    'header': { height: '100px', width: '100%' },
24    'products': { height: '300px', width: '100%' },
25    'sidebar': { height: '400px', width: '250px' },
26    // ... other types
27  };
28
29  return {
30    height: userDimensions.height || defaults[type]?.height || '200px',
31    width: userDimensions.width || defaults[type]?.width || '100%'
32  };
33}

Typical Data Streaming Use Cases

Data streaming is particularly useful in the following scenarios:

1. E-commerce Applications

  • Fast loading of basic product information
  • Asynchronous loading of reviews and recommendations
  • Interactive filters and search during product loading

2. Analytical Dashboards

  • Immediate loading of critical metrics
  • Gradual loading of complex charts and reports
  • Independent analytics sections with their own loading states

3. Content Aggregators

  • Fast loading of headlines and images
  • Delayed loading of article content
  • Prioritizing current content over archived content

4. Social Media and Chat Applications

  • Fast loading of basic profile information
  • Streaming messages and comments
  • Loading media in the background after text is loaded

Summary

Data streaming and React Suspense in Next.js App Router offer powerful tools for creating more performant and responsive applications. The key advantages of this approach are:

  1. Better user experience - content is displayed gradually, giving the impression of faster application performance
  2. TTFB and LCP optimization - critical UI parts load faster, improving key performance metrics
  3. Granular control - the ability to precisely control the order of content loading
  4. Flexible UI management - better management of loading states for different page sections

When implementing data streaming in your Next.js applications, remember the following best practices:

  1. Divide the UI into logical, independent parts - each part should be wrapped in its own Suspense component
  2. Prioritize critical content - the most important elements should be loaded first
  3. Parallel data fetching - initialize queries early to avoid waterfall loading
  4. Ensure smooth loading states - use loading skeletons with appropriate dimensions to avoid layout shifts
  5. Handle errors properly - use Error Boundaries for each section to isolate problems

In the next module, we will discuss in detail how to create advanced loading skeletons and progress indicators that complement the data streaming strategy, ensuring an even better user experience.

Go to CodeWorlds