Files
Portfolio/blog-posts/30-streaming-ui-patterns.md
damjan_savicandClaude Opus 4.5 43484c5023 Add blog posts, cleanup unused files, update components
- Add 100 blog posts covering AI, development, and tech topics
- Add .env.example for environment configuration
- Add accessibility and lighthouse audit scripts
- Remove obsolete SEO reports and temporary files
- Remove dev-dist build artifacts and backup files
- Remove unused portrait images (moved/consolidated elsewhere)
- Update contact form and component improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 11:42:11 +01:00

496 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Streaming UI Patterns: Progressive Loading für moderne Web-Apps
**Meta-Description:** Design Patterns für Streaming UI mit React Suspense. Skeleton Loading, Progressive Enhancement und optimale User Experience bei langsamen Daten.
**Keywords:** Streaming UI, Suspense, Skeleton Loading, Progressive Loading, React Streaming, SSR Streaming, Loading States
---
## Einführung
Streaming UI bedeutet: **Zeige sofort was du hast, streame den Rest nach**. Statt einer leeren Seite oder einem Spinner sieht der User sofort Inhalte während langsame Daten im Hintergrund laden.
---
## Das Streaming-Prinzip
```
┌─────────────────────────────────────────────────────────────┐
│ STREAMING VS. BLOCKING │
├─────────────────────────────────────────────────────────────┤
│ │
│ Blocking (Traditionell): │
│ ┌────────────────────────────────────────────────────┐ │
│ │ [Spinner 3s] → [Komplette Seite] │ │
│ └────────────────────────────────────────────────────┘ │
│ User wartet 3 Sekunden auf irgendetwas │
│ │
│ Streaming (Modern): │
│ ┌────────────────────────────────────────────────────┐ │
│ │ [Header] ─ sofort │ │
│ │ [Nav] ─ sofort │ │
│ │ [Hero] ─ 100ms │ │
│ │ [Stats] ─ [Skeleton] → [Daten] ─ 500ms │ │
│ │ [Chart] ─ [Skeleton] → [Daten] ─ 1s │ │
│ │ [Table] ─ [Skeleton] → [Daten] ─ 2s │ │
│ │ [Footer] ─ sofort │ │
│ └────────────────────────────────────────────────────┘ │
│ User sieht sofort Inhalte, Details laden progressiv │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## Pattern 1: Suspense Boundaries
### Strategische Platzierung
```tsx
// app/dashboard/page.tsx
import { Suspense } from 'react';
export default function DashboardPage() {
return (
<main>
{/* Sofort gerendert - keine Suspense */}
<Header />
<Navigation />
{/* Schnelle Daten */}
<Suspense fallback={<StatsSkeleton />}>
<QuickStats />
</Suspense>
<div className="grid grid-cols-2 gap-4">
{/* Mittlere Latenz - unabhängig voneinander */}
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<TrafficChart />
</Suspense>
</div>
{/* Langsame Daten - lädt zuletzt */}
<Suspense fallback={<TableSkeleton rows={10} />}>
<DetailedReports />
</Suspense>
{/* Statisch */}
<Footer />
</main>
);
}
```
### Nested Suspense für feinere Kontrolle
```tsx
function ProductSection() {
return (
<section>
<Suspense fallback={<ProductListSkeleton />}>
<ProductList />
{/* Nested: Reviews laden nach Produkten */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews />
</Suspense>
</Suspense>
</section>
);
}
```
---
## Pattern 2: Skeleton Components
### Anatomy-preserving Skeletons
```tsx
// components/skeletons/ProductCardSkeleton.tsx
export function ProductCardSkeleton() {
return (
<div className="border rounded-lg p-4 animate-pulse">
{/* Bild */}
<div className="h-48 bg-gray-200 rounded-lg mb-4" />
{/* Titel */}
<div className="h-6 bg-gray-200 rounded w-3/4 mb-2" />
{/* Beschreibung */}
<div className="space-y-2 mb-4">
<div className="h-4 bg-gray-200 rounded w-full" />
<div className="h-4 bg-gray-200 rounded w-5/6" />
</div>
{/* Preis + Button */}
<div className="flex justify-between items-center">
<div className="h-8 bg-gray-200 rounded w-24" />
<div className="h-10 bg-gray-200 rounded w-28" />
</div>
</div>
);
}
// Grid Skeleton
export function ProductGridSkeleton({ count = 6 }) {
return (
<div className="grid grid-cols-3 gap-4">
{Array.from({ length: count }).map((_, i) => (
<ProductCardSkeleton key={i} />
))}
</div>
);
}
```
### Shimmer Effect
```css
/* styles/skeleton.css */
.skeleton-shimmer {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
```
```tsx
// Skeleton mit Shimmer
export function ShimmerSkeleton({ className }: { className: string }) {
return <div className={`skeleton-shimmer ${className}`} />;
}
```
---
## Pattern 3: Optimistic Updates
### Sofortige UI-Reaktion
```tsx
'use client';
import { useOptimistic, useTransition } from 'react';
import { addToFavorites } from './actions';
function FavoriteButton({ productId, initialFavorited }) {
const [isPending, startTransition] = useTransition();
// Optimistic State
const [optimisticFavorited, addOptimistic] = useOptimistic(
initialFavorited,
(current, newValue: boolean) => newValue
);
const handleClick = () => {
// Sofort UI updaten
addOptimistic(!optimisticFavorited);
// Server-Action im Hintergrund
startTransition(async () => {
await addToFavorites(productId, !optimisticFavorited);
});
};
return (
<button
onClick={handleClick}
className={optimisticFavorited ? 'text-red-500' : 'text-gray-400'}
>
{optimisticFavorited ? '❤️' : '🤍'}
{isPending && <span className="ml-1">...</span>}
</button>
);
}
```
---
## Pattern 4: Loading Hierarchies
### loading.tsx für Route-Level
```tsx
// app/products/loading.tsx
export default function ProductsLoading() {
return (
<div>
{/* Header ist statisch */}
<div className="h-8 w-48 bg-gray-200 rounded mb-6" />
{/* Filter Bar Skeleton */}
<div className="flex gap-2 mb-4">
<div className="h-10 w-32 bg-gray-200 rounded" />
<div className="h-10 w-32 bg-gray-200 rounded" />
<div className="h-10 w-32 bg-gray-200 rounded" />
</div>
{/* Product Grid Skeleton */}
<ProductGridSkeleton count={9} />
</div>
);
}
```
### Hierarchische Loading States
```tsx
// Layout mit persistentem Skeleton
export default function ProductsLayout({
children
}: {
children: React.ReactNode
}) {
return (
<div>
{/* Immer sichtbar */}
<ProductsHeader />
<FilterSidebar />
{/* Content lädt */}
<main className="flex-1">
{children}
</main>
</div>
);
}
```
---
## Pattern 5: Streaming mit LLM
### AI-Response Streaming UI
```tsx
'use client';
import { useStreamingChat } from '@/hooks/useStreamingChat';
function ChatInterface() {
const { response, isStreaming, sendMessage } = useStreamingChat();
const [input, setInput] = useState('');
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto p-4">
{/* Streaming Response */}
{(response || isStreaming) && (
<div className="bg-gray-100 rounded-lg p-4">
{response}
{isStreaming && (
<span className="inline-block w-2 h-4 bg-blue-500 ml-1 animate-pulse" />
)}
</div>
)}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage(input);
setInput('');
}}
className="p-4 border-t"
>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={isStreaming}
className="w-full p-2 border rounded"
/>
</form>
</div>
);
}
```
### Partial Content während Streaming
```tsx
function AIAnalysisCard() {
const { analysis, isAnalyzing, sections } = useAIAnalysis();
return (
<div className="space-y-4">
{/* Sections erscheinen progressiv */}
{sections.map((section, i) => (
<div
key={i}
className={`transition-opacity duration-300 ${
section.loaded ? 'opacity-100' : 'opacity-0'
}`}
>
<h3>{section.title}</h3>
{section.loaded ? (
<p>{section.content}</p>
) : (
<div className="h-20 skeleton-shimmer rounded" />
)}
</div>
))}
{isAnalyzing && (
<div className="text-sm text-gray-500">
Analysiere... {sections.filter(s => s.loaded).length}/{sections.length}
</div>
)}
</div>
);
}
```
---
## Pattern 6: Error Boundaries mit Fallback
```tsx
// components/ErrorBoundary.tsx
'use client';
import { useEffect } from 'react';
export function ErrorBoundary({
error,
reset
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
console.error('Error:', error);
}, [error]);
return (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<h2 className="text-red-800 font-semibold">
Etwas ist schiefgelaufen
</h2>
<p className="text-red-600 text-sm mt-1">
{error.message}
</p>
<button
onClick={reset}
className="mt-3 px-4 py-2 bg-red-100 text-red-800 rounded hover:bg-red-200"
>
Erneut versuchen
</button>
</div>
);
}
// error.tsx für Route-Level
export default function ProductsError({
error,
reset
}: {
error: Error;
reset: () => void;
}) {
return <ErrorBoundary error={error} reset={reset} />;
}
```
---
## Performance-Metriken
| Metrik | Blocking | Streaming | Verbesserung |
|--------|----------|-----------|--------------|
| **FCP** | 3.2s | 0.8s | 75% schneller |
| **LCP** | 3.5s | 1.2s | 66% schneller |
| **TTI** | 4.0s | 2.0s | 50% schneller |
| **CLS** | 0.15 | 0.02 | 87% besser |
---
## Best Practices
### Do's
```tsx
// ✅ Unabhängige Suspense Boundaries
<Suspense fallback={<A />}><ComponentA /></Suspense>
<Suspense fallback={<B />}><ComponentB /></Suspense>
// ✅ Skeleton passt zur echten Komponente
<Suspense fallback={<ProductCardSkeleton />}>
<ProductCard />
</Suspense>
// ✅ Static Content außerhalb von Suspense
<Header /> {/* Kein Suspense nötig */}
<Suspense>
<DynamicContent />
</Suspense>
<Footer /> {/* Kein Suspense nötig */}
```
### Don'ts
```tsx
// ❌ Eine große Suspense für alles
<Suspense fallback={<FullPageSpinner />}>
<EntirePage />
</Suspense>
// ❌ Generischer Spinner statt Skeleton
<Suspense fallback={<Spinner />}>
<ComplexDashboard />
</Suspense>
// ❌ Suspense um statischen Content
<Suspense>
<StaticHeader />
</Suspense>
```
---
## Fazit
Streaming UI Patterns verbessern UX dramatisch:
1. **Sofortige Inhalte**: User sieht nie eine leere Seite
2. **Progressive Loading**: Wichtiges zuerst, Details später
3. **Perceived Performance**: App fühlt sich schneller an
4. **Resilience**: Langsame Teile blockieren nicht den Rest
Die Zukunft ist nicht "Laden oder Geladen" es ist ein Spektrum.
---
## Bildprompts
1. "Website loading progressively in sections, waterfall effect, UI/UX concept"
2. "Skeleton screens transforming into real content, animation sequence"
3. "User happily browsing while content streams in background, modern web experience"
---
## Quellen
- [React Suspense Documentation](https://react.dev/reference/react/Suspense)
- [Next.js Loading UI](https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming)
- [Vercel: Streaming SSR](https://vercel.com/blog/streaming-server-rendering-with-suspense)
- [Web.dev: Skeleton Screens](https://web.dev/articles/skeleton-screens)