Next.js course Β· Module 5: Rendering Strategies
Data Streaming and React Suspense
In this lesson10
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:
- Blocking rendering - classically, the entire page must wait until all data is fetched before anything is displayed to the user
- Waterfall data fetching - sequential data fetching where each request must wait for the previous one to complete
- 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:
- Faster TTFB (Time To First Byte) - the user sees the first content sooner
- Progressive rendering - the UI is rendered gradually, giving a sense of faster responsiveness
- 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:
- Allowing components to "pause" rendering while waiting for data
- Displaying a fallback component during loading
- 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:
- The dashboard page is divided into independent sections
- Each section is wrapped in a
Suspensecomponent with an appropriate fallback - 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:
- The outer
Suspensewraps the entire category view - Inner
Suspensecomponents split the page into smaller, independently loading sections - 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:
ProductImageGalleryandAddToCartButtonare client components ('use client')- We wrap client components in asynchronous server components to pass them data
- The
AddToCartButtoncomponent 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:
- We initialize data fetching already in the
generateMetadatafunction, before components request it - We use
voidto ignore the Promise, since we don't need its result in this function - 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:
- We establish a clear priority hierarchy for page content
- The most critical elements (header, content) are streamed first
- Less critical elements (comments, related articles) are streamed later
- 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:
- They can asynchronously fetch data directly from databases or APIs
- They are rendered on the server and sent to the client as HTML
- They can be wrapped in
Suspensefor 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:
- They cannot be directly asynchronous
- They use React hooks (useState, useEffect) to fetch data
- 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:
- Better user experience - content is displayed gradually, giving the impression of faster application performance
- TTFB and LCP optimization - critical UI parts load faster, improving key performance metrics
- Granular control - the ability to precisely control the order of content loading
- 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:
- Divide the UI into logical, independent parts - each part should be wrapped in its own Suspense component
- Prioritize critical content - the most important elements should be loaded first
- Parallel data fetching - initialize queries early to avoid waterfall loading
- Ensure smooth loading states - use loading skeletons with appropriate dimensions to avoid layout shifts
- 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.
Code for this lesson: App.tsx
1import React, { useState, useEffect, Suspense } from 'react';
2
3// Simulation of Streaming and React.Suspense in Next.js
4
5function SlowComponent({ label, delay }: { label: string; delay: number }) {
6 const [data, setData] = useState<string | null>(null);
7
8 useEffect(() => {
9 const timer = setTimeout(() => {
10 setData(`Data loaded after ${delay}ms`);
11 }, delay);
12 return () => clearTimeout(timer);
13 }, [delay]);
14
15 if (!data) throw new Promise(() => {}); // pending simulation
16 return (
17 <div style={{ background: '#1a2744', border: '1px solid #64ffda', borderRadius: 8, padding: 16, marginBottom: 12 }}>
18 <h3 style={{ color: '#64ffda', margin: 0 }}>{label}</h3>
19 <p style={{ color: '#ccc', margin: '8px 0 0' }}>{data}</p>
20 </div>
21 );
22}
23
24// Loading component (fallback for Suspense)
25function StreamingFallback({ label }: { label: string }) {
26 return (
27 <div style={{
28 background: '#1a1a2e', border: '1px dashed #555',
29 borderRadius: 8, padding: 16, marginBottom: 12
30 }}>
31 <div style={{ color: '#888' }}>Streaming: {label}...</div>
32 <div style={{
33 height: 12, background: 'linear-gradient(90deg, #333, #555, #333)',
34 borderRadius: 4, marginTop: 8, animation: 'pulse 1.5s infinite'
35 }} />
36 </div>
37 );
38}
39
40// Demo component with different loading times
41function DemoSection({ label, delay }: { label: string; delay: number }) {
42 const [key, setKey] = useState(0);
43 return (
44 <div>
45 <button onClick={() => setKey(k => k + 1)} style={{
46 background: '#2196f3', color: '#fff', border: 'none',
47 borderRadius: 4, padding: '4px 10px', marginBottom: 8, cursor: 'pointer', fontSize: 11
48 }}>
49 Reload {label}
50 </button>
51 <ErrorBoundary key={key} fallback={<StreamingFallback label={label} />}>
52 <Suspense fallback={<StreamingFallback label={label} />}>
53 <SlowComponent label={label} delay={delay} key={key} />
54 </Suspense>
55 </ErrorBoundary>
56 </div>
57 );
58}
59
60// Simple error boundary
61class ErrorBoundary extends React.Component<
62 { children: React.ReactNode; fallback: React.ReactNode },
63 { hasError: boolean }
64> {
65 state = { hasError: false };
66 static getDerivedStateFromError() { return { hasError: true }; }
67 render() {
68 return this.state.hasError ? this.props.fallback : this.props.children;
69 }
70}
71
72export default function StreamingDemo() {
73 return (
74 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: 20, color: '#e0e0e0', fontFamily: 'monospace' }}>
75 <h1 style={{ color: '#64ffda' }}>Streaming and Suspense</h1>
76 <p style={{ color: '#888', marginBottom: 20 }}>
77 Each section loads independently - that's exactly streaming!
78 </p>
79 <DemoSection label="Navigation (fast)" delay={500} />
80 <DemoSection label="Product list (medium)" delay={1500} />
81 <DemoSection label="AI recommendations (slow)" delay={3000} />
82 </div>
83 );
84}Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. SSG in Next.js is:
2. ISR in Next.js means:
Hands-on tasks in the game
- Code editor
Implement a dashboard with Streaming SSR and Suspense
- Vertical ordering
Arrange the Server Component rendering stages in correct order:
- Click in order
Arrange the fetch syntax with revalidate option in Next.js
- Code editor
Create a Server Component that asynchronously fetches data and displays it on the page