Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Streaming danych i React Suspense

W nowoczesnym rozwoju aplikacji webowych wydajność i responsywność interfejsu użytkownika są kluczowymi czynnikami wpływającymi na doświadczenie użytkownika. Next.js, jako framework frontendowy, wprowadza zaawansowane techniki, które pomagają osiągnąć płynne doświadczenie użytkownika nawet przy złożonych operacjach pobierania danych. W tym module przyjrzymy się, jak Next.js App Router wykorzystuje streaming danych i React Suspense do tworzenia bardziej responsywnych aplikacji.

Problemy tradycyjnego renderowania

Przed przejściem do streamingu danych, warto zrozumieć ograniczenia tradycyjnych podejść do renderowania:

  1. Renderowanie blokujące - klasycznie, cała strona musi poczekać, aż wszystkie dane zostaną pobrane, zanim cokolwiek zostanie wyświetlone użytkownikowi
  2. Waterfall data fetching - sekwencyjne pobieranie danych, gdzie każde żądanie musi poczekać na zakończenie poprzedniego
  3. Wszystko albo nic - cała strona jest renderowana jako jedna niepodzielna całość

Te ograniczenia prowadzą do wolniejszego czasu do pierwszej treści (TTFB - Time To First Byte) i mogą powodować frustrację użytkowników, szczególnie na wolniejszych połączeniach lub przy skomplikowanych zapytaniach do bazy danych.

Streaming danych - nowe podejście

Streaming danych, wprowadzony w Next.js App Router, pozwala na stopniowe przesyłanie fragmentów UI z serwera do klienta w miarę ich gotowości, zamiast czekać na zakończenie wszystkich operacji pobierania danych.

Kluczowe zalety tego podejścia to:

  1. Szybszy TTFB (Time To First Byte) - użytkownik widzi pierwszą treść szybciej
  2. Progresywny rendering - UI jest renderowany stopniowo, co daje poczucie szybszej responsywności
  3. Unikanie blokowania - wolniejsze komponenty nie blokują renderowania szybszych komponentów

React Suspense - fundament streamingu w React

Suspense to funkcja React wprowadzona w wersji 16.6, która umożliwia "zawieszenie" renderowania komponentu do czasu spełnienia określonego warunku (np. załadowania danych). Next.js App Router w pełni wykorzystuje tę funkcjonalność do implementacji streamingu danych.

Podstawową ideą Suspense jest:

  1. Umożliwienie komponentom "wstrzymania" renderowania podczas oczekiwania na dane
  2. Wyświetlenie komponentu zastępczego (fallback) podczas ładowania
  3. Zastąpienie fallbacku właściwym komponentem, gdy dane są gotowe

Implementacja streamingu danych w Next.js App Router

1. Podstawowy przykład użycia React Suspense

Zacznijmy od podstawowego przykładu, który pokazuje, jak używać Suspense w 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>Panel użytkownika</h1>
12      
13      {/* Natychmiast renderowany nagłówek */}
14      <div className="dashboard-header">
15        <Suspense fallback={<Loading />}>
16          <UserProfile />
17        </Suspense>
18      </div>
19      
20      <div className="dashboard-grid">
21        {/* Sekcja zamówień z własnym stanem ładowania */}
22        <div className="orders-section">
23          <Suspense fallback={<Loading type="orders" />}>
24            <RecentOrders />
25          </Suspense>
26        </div>
27        
28        {/* Sekcja analityki z własnym stanem ładowania */}
29        <div className="analytics-section">
30          <Suspense fallback={<Loading type="analytics" />}>
31            <Analytics />
32          </Suspense>
33        </div>
34      </div>
35    </div>
36  );
37}

W tym przykładzie:

  1. Strona dashboardu jest podzielona na niezależne sekcje
  2. Każda sekcja jest owinięta w komponent
    Suspense
    z odpowiednim fallbackiem
  3. Sekcje są renderowane niezależnie, gdy ich dane są gotowe

Komponenty

UserProfile
,
RecentOrders
i
Analytics
mogą asynchronicznie pobierać dane:

1// app/dashboard/user-profile.tsx
2async function getUserData() {
3  // Pobieranie danych użytkownika - może zająć 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  // Pobieranie ostatnich zamówień - może zająć 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>Ostatnie zamówienia</h3>
14      <table>
15        <thead>
16          <tr>
17            <th>ID</th>
18            <th>Produkt</th>
19            <th>Cena</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. Zagnieżdżone komponenty Suspense

Możemy tworzyć bardziej złożone struktury z zagnieżdżonymi komponentami Suspense:

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      {/* Zagnieżdżone komponenty Suspense */}
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}

W tym przykładzie:

  1. Zewnętrzny
    Suspense
    obejmuje cały widok kategorii
  2. Wewnętrzne komponenty
    Suspense
    dzielą stronę na mniejsze, niezależnie ładowane sekcje
  3. Każda sekcja ma własny stan ładowania dostosowany do typu treści

3. Streaming z komponentami klienta

Możemy łączyć streaming z komponentami klienta, co daje nam możliwość tworzenia interaktywnych UI z płynnym ładowaniem:

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'; // Komponent klienta
6import ProductImageGallery from './product-image-gallery'; // Komponent klienta
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          {/* Komponent klienta dla interaktywności */}
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 komponentów, które potrzebują 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}

W tym przykładzie:

  1. ProductImageGallery
    i
    AddToCartButton
    to komponenty klienta (
    'use client'
    )
  2. Wrapujemy komponenty klienta w asynchroniczne komponenty serwera, aby przekazać im dane
  3. Komponent
    AddToCartButton
    jest interaktywny od razu po załadowaniu, nawet gdy inne części strony są wciąż w trakcie ładowania

Zaawansowane techniki streamingu danych

1. Równoległe pobieranie danych

Jedną z najlepszych praktyk przy streamingu danych jest paralelizacja żądań, aby uniknąć wodospadowego pobierania danych:

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// Preinicjalizacja zapytań - unikanie wodospadu danych
9export function generateMetadata() {
10  // Inicjujemy pobieranie danych wcześnie, aby były gotowe
11  // gdy komponenty będą ich potrzebować
12  preloadUserData();
13  preloadRecentOrders();
14  preloadAnalytics();
15  
16  return {
17    title: 'Dashboard - Moja Aplikacja',
18  };
19}
20
21// Funkcje preinicjalizujące
22function preloadUserData() {
23  void getUserData(); // void ignoruje Promise
24}
25
26function preloadRecentOrders() {
27  void getRecentOrders(); // void ignoruje Promise
28}
29
30function preloadAnalytics() {
31  void getAnalyticsData(); // void ignoruje Promise
32}
33
34// Funkcje pobierające dane z 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      {/* ... struktura strony z komponentami Suspense, jak wcześniej ... */}
54    </div>
55  );
56}

W tym podejściu:

  1. Inicjujemy pobieranie danych już w funkcji
    generateMetadata
    , zanim komponenty ich zażądają
  2. Używamy
    void
    do ignorowania Promise, ponieważ nie potrzebujemy ich rezultatu w tej funkcji
  3. W komponentach używamy tych samych funkcji pobierających dane, które korzystają z cache

2. Kaskadowe Suspense - priorytetyzacja zawartości

Możemy strategicznie priorytetyzować ładowanie treści, aby najważniejsze elementy były wyświetlane jako pierwsze:

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      {/* Priorytet 1: Nagłówek artykułu - ładuje się pierwszy */}
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          {/* Priorytet 2: Treść artykułu */}
22          <Suspense fallback={<Loading type="content" />}>
23            <ArticleContent slug={(await params).slug} />
24            
25            <div className="article-footer">
26              {/* Priorytet 3: Autor i udostępnianie - ładują się po treści */}
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          {/* Priorytet 4: Powiązane artykuły - ładują się na końcu */}
38          <Suspense fallback={<Loading type="related" />}>
39            <RelatedArticles slug={(await params).slug} />
40          </Suspense>
41        </aside>
42      </div>
43      
44      {/* Priorytet 5: Komentarze - ładują się na samym końcu */}
45      <Suspense fallback={<Loading type="comments" />}>
46        <ArticleComments slug={(await params).slug} />
47      </Suspense>
48    </article>
49  );
50}

W tym przykładzie:

  1. Ustalamy jasną hierarchię priorytetów dla zawartości strony
  2. Najbardziej krytyczne elementy (nagłówek, treść) są streamowane jako pierwsze
  3. Mniej krytyczne elementy (komentarze, powiązane artykuły) są streamowane później
  4. Zagnieżdżając komponenty Suspense, tworzymy naturalny przepływ ładowania treści

3. Streaming komponentów z danymi zależnymi

Czasami potrzebujemy, aby jeden komponent poczekał na dane z innego komponentu. Możemy to zrealizować za pomocą kontekstu i współdzielonych zasobów:

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          {/* Ten komponent potrzebuje danych z 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}

W komponentach odbierających dane:

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>Ładowanie informacji o kategorii...</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} produktów</span>
19        <span>{categoryData.brandsCount} marek</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    // Czekamy, aż categoryData będzie dostępne
14    if (!categoryData) return;
15    
16    async function loadProducts() {
17      try {
18        // Używamy categoryData do pobierania produktów
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>Ładowanie produktów...</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 w kontekście streamingu

Przy implementacji streamingu danych w Next.js App Router, ważne jest zrozumienie różnic między komponentami serwera i klienta:

Server Components

Komponenty serwera są idealne do streamingu, ponieważ:

  1. Mogą asynchronicznie pobierać dane bezpośrednio z baz danych lub API
  2. Są renderowane na serwerze i przesyłane do klienta jako HTML
  3. Mogą być wrappowane w
    Suspense
    dla obsługi stanu ładowania
1// app/products/product-list.tsx (Server Component)
2async function getProducts() {
3  // Bezpośredni dostęp do bazy danych na serwerze
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

Komponenty klienta są potrzebne dla interaktywności, ale mają inne zachowanie w kontekście streamingu:

  1. Nie mogą być bezpośrednio asynchroniczne
  2. Używają hooków React (useState, useEffect) do pobierania danych
  3. Są renderowane na kliencie i wymagają przesłania JS do przeglądarki
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>Ładowanie filtrów...</div>;
33  
34  return (
35    <div className="filter-panel">
36      <h3>Kategorie</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}

Łączenie Server i Client Components

Najlepszą praktyką jest używanie Server Components do pobierania danych i przygotowywania zawartości, a Client Components do interaktywności:

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={/* obsługa po stronie klienta */} />
12      </div>
13      
14      <div className="main-content">
15        <Suspense fallback={<Loading type="products" />}>
16          <ProductList />
17        </Suspense>
18      </div>
19    </div>
20  );
21}

Obsługa błędów w streamingu danych

Podczas streamingu danych, istotna jest odpowiednia obsługa błędów. Next.js App Router dostarcza komponent

Error Boundary
do obsługi błędów w strumieniowanych komponentach:

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    // Logowanie błędu do serwisu monitorującego
15    console.error('Dashboard error:', error);
16  }, [error]);
17  
18  return (
19    <div className="error-container">
20      <h2>Coś poszło nie tak!</h2>
21      <p>{error.message || 'Wystąpił nieoczekiwany błąd.'}</p>
22      <button 
23        onClick={() => reset()}
24        className="button"
25      >
26        Spróbuj ponownie
27      </button>
28    </div>
29  );
30}

Możemy także tworzyć komponent Error Boundary dla konkretnych sekcji:

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>Błąd ładowania analityki</h3>
8      <p>Nie mogliśmy załadować danych analitycznych: {error.message}</p>
9      <button onClick={reset}>Spróbuj ponownie</button>
10    </div>
11  );
12}

A następnie używać go dla konkretnych sekcji:

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      {/* ... inne sekcje ... */}
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}

Optymalizacja wydajności streamingu

Właściwa implementacja streamingu danych może znacząco poprawić wydajność aplikacji. Oto kilka dodatkowych technik optymalizacyjnych:

1. Zapobieganie migotaniu interfejsu

1// app/layout.tsx
2export default function RootLayout({ children }) {
3  return (
4    <html lang="pl">
5      <body>
6        {/* Ustawienie minimalnego opóźnienia dla Suspense */}
7        <script dangerouslySetInnerHTML={{ __html: `
8          window.SuspenseMinDelay = 300; // ms
9        ` }} />
10        {children}
11      </body>
12    </html>
13  );
14}

Możemy również stworzyć własny komponent do opóźniania stanu ładowania:

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. Priorytetyzacja krytycznych danych

1// app/products/[id]/page.tsx
2import { Suspense } from 'react';
3import { ProductBasicInfo } from './product-basic-info'; // Krytyczne dane
4import { ProductDetails } from './product-details'; // Ważne dane
5import { ProductReviews } from './product-reviews'; // Mniej krytyczne dane
6import { RelatedProducts } from './related-products'; // Najmniej krytyczne dane
7import { Loading } from '@/components/loading';
8
9export default async function ProductPage({ params }) {
10  // Preinicjalizacja krytycznych danych
11  preloadProductBasicInfo((await params).id);
12  
13  return (
14    <div className="product-page">
15      {/* Natychmiastowe renderowanie krytycznych informacji */}
16      <div className="product-hero">
17        <Suspense fallback={<Loading priority="high" />}>
18          <ProductBasicInfo productId={(await params).id} />
19        </Suspense>
20      </div>
21      
22      {/* Mniej krytyczne sekcje ładowane później */}
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// Preinicjalizacja krytycznych danych
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' // Ustawienie priorytetu fetch API
52  });
53  return res.json();
54}

3. Ograniczanie przeskoków układu (CLS)

1// components/loading.tsx
2export function Loading({ type, priority = 'medium', height, width }) {
3  // Określamy wymiary szkieletu na podstawie typu zawartości
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        // Predefiniowane wymiary zapobiegają przeskokom layoutu
13      }}
14    >
15      <div className="skeleton-animation" />
16    </div>
17  );
18}
19
20function getDimensionsForType(type, userDimensions) {
21  // Domyślne wymiary dla różnych typów treści
22  const defaults = {
23    'header': { height: '100px', width: '100%' },
24    'products': { height: '300px', width: '100%' },
25    'sidebar': { height: '400px', width: '250px' },
26    // ... inne typy
27  };
28  
29  return {
30    height: userDimensions.height || defaults[type]?.height || '200px',
31    width: userDimensions.width || defaults[type]?.width || '100%'
32  };
33}

Typowe przypadki użycia streamingu danych

Streaming danych jest szczególnie przydatny w następujących scenariuszach:

1. Aplikacje e-commerce

  • Szybkie ładowanie podstawowych informacji o produkcie
  • Asynchroniczne ładowanie recenzji i rekomendacji
  • Interaktywne filtry i wyszukiwanie podczas ładowania produktów

2. Dashboardy analityczne

  • Natychmiastowe ładowanie krytycznych wskaźników
  • Stopniowe ładowanie złożonych wykresów i raportów
  • Niezależne sekcje analityczne z własnymi stanami ładowania

3. Agregatory treści

  • Szybkie ładowanie nagłówków i obrazów
  • Opóźnione ładowanie treści artykułów
  • Priorytetyzacja aktualnych treści przed archiwalnymi

4. Social media i aplikacje czatu

  • Szybkie ładowanie podstawowych informacji profilu
  • Strumieniowanie wiadomości i komentarzy
  • Ładowanie mediów w tle po załadowaniu tekstu

Podsumowanie

Streaming danych i React Suspense w Next.js App Router oferują potężne narzędzia do tworzenia wydajniejszych i bardziej responsywnych aplikacji. Kluczowe zalety tego podejścia to:

  1. Lepsze doświadczenie użytkownika - treść jest wyświetlana stopniowo, co daje wrażenie szybszego działania aplikacji
  2. Optymalizacja TTFB i LCP - krytyczne części UI ładują się szybciej, poprawiając kluczowe metryki wydajności
  3. Granularna kontrola - możliwość precyzyjnego sterowania kolejnością ładowania treści
  4. Elastyczne zarządzanie UI - lepsze zarządzanie stanami ładowania dla różnych sekcji strony

Implementując streaming danych w swoich aplikacjach Next.js, pamiętaj o następujących najlepszych praktykach:

  1. Dziel UI na logiczne, niezależne części - każda część powinna być owinięta własnym komponentem Suspense
  2. Priorytetyzuj krytyczne treści - najważniejsze elementy powinny być ładowane jako pierwsze
  3. Równoległe pobieranie danych - inicjalizuj zapytania wcześnie, aby uniknąć wodospadowego ładowania
  4. Zapewnij płynne stany ładowania - stosuj szkielety ładowania o odpowiednich wymiarach, by uniknąć przeskoków layoutu
  5. Odpowiednio obsługuj błędy - używaj Error Boundary dla każdej sekcji, aby izolować problemy

W następnym module omówimy szczegółowo, jak tworzyć zaawansowane szkielety ładowania i wskaźniki postępu, które uzupełniają strategię streamingu danych, zapewniając jeszcze lepsze doświadczenie użytkownika.

Przejdź do CodeWorlds