Streaming — Suspense bilan, foydalanuvchi tajribasini yaxshilash

~15 daq · Next.js · Rendering Strategiyalari

Muammodan boshlaylik

Siz dashboard sahifasini yozdingiz. Sahifada 3 ta qism bor:

  • Foydalanuvchi ma'lumotlari — DB dan, 50ms
  • So'nggi buyurtmalar — DB dan, 200ms
  • Tavsiya etilgan mahsulotlar — tashqi API dan, 800ms

Dynamic Rendering da Next.js hammasi tayyor bo'lguncha kutadi — 800ms. Foydalanuvchi 800ms oq ekran ko'radi. Holbuki foydalanuvchi ma'lumotlari 50ms da tayyor edi.

50ms  → foydalanuvchi ma'lumoti tayyor   (kutmoqda...)
200ms → buyurtmalar tayyor               (kutmoqda...)
800ms → tavsiyalar tayyor                → sahifa ko'rinadi

Bu — yomon tajriba. Streaming bu muammoni hal qiladi.


Streaming qanday ishlaydi

Streaming — sahifani bo'laklarga bo'lib yuborish. Tayyor bo'lgan qism darhol foydalanuvchiga yuboriladi, qolganlar kelguncha loading ko'rsatiladi.

50ms  → foydalanuvchi ma'lumoti → darhol ko'rinadi
200ms → buyurtmalar             → ko'rinadi
800ms → tavsiyalar              → ko'rinadi

Foydalanuvchi 50ms da birinchi kontent ko'radi. 800ms kutmaydi.


Suspense bilan Streaming

React ning Suspense komponenti — "bu qism yuklanayotganda fallback ko'rsat" degani. Next.js bu ustiga Streaming qurgan.

// app/dashboard/page.tsx
import { Suspense } from 'react'
import { UserInfo } from './UserInfo'
import { RecentOrders } from './RecentOrders'
import { Recommendations } from './Recommendations'

export default function DashboardPage() {
  return (
    <div>
      {/* 50ms — tez, Suspense shart emas */}
      <Suspense fallback={<p>Yuklanmoqda...</p>}>
        <UserInfo />
      </Suspense>

      {/* 200ms */}
      <Suspense fallback={<OrdersSkeleton />}>
        <RecentOrders />
      </Suspense>

      {/* 800ms — sekin, lekin boshqalarni to'sib qo'ymaydi */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations />
      </Suspense>
    </div>
  )
}

Har bir komponent mustaqil yuklanadi. Recommendations 800ms kutayotganda UserInfo va RecentOrders allaqachon ko'ringan bo'ladi.


Suspense ichidagi komponentlar async bo'lishi kerak

Suspense faqat async Server Component larni "kutadi". Komponent await qilayotganda — fallback ko'rsatiladi, tugaganda — kontent.

// UserInfo.tsx — Server Component
export async function UserInfo() {
  const user = await db.getUser()  // 50ms
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  )
}
// Recommendations.tsx — Server Component
export async function Recommendations() {
  const items = await fetch('https://api.example.com/recommend', {
    cache: 'no-store'
  }).then(r => r.json())  // 800ms

  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  )
}

Skeleton UI — yaxshi fallback

fallback da oddiy "Yuklanmoqda..." matn yozish mumkin. Lekin Skeleton UI — kontent shakliga o'xshash kulrang bloklar — foydalanuvchi tajribasini yaxshilaydi.

function OrdersSkeleton() {
  return (
    <div className="animate-pulse">
      {[1, 2, 3].map(i => (
        <div key={i} className="flex gap-4 mb-4">
          <div className="h-12 w-12 bg-gray-200 rounded" />
          <div className="flex-1">
            <div className="h-4 bg-gray-200 rounded mb-2" />
            <div className="h-3 bg-gray-200 rounded w-2/3" />
          </div>
        </div>
      ))}
    </div>
  )
}

animate-pulse — Tailwind klassi, kulrang bloklar yumshoq miltillaydi.


loading.tsx vs Suspense — farqi

Oldingi darsda loading.tsx ni o'rgandik. U ham Streaming ishlatadi — lekin butun sahifa uchun.

loading.tsx  → butun page.tsx yuklanguncha
Suspense     → faqat o'sha komponent yuklanguncha
// loading.tsx — butun sahifa uchun
export default function Loading() {
  return <FullPageSkeleton />
}
// Suspense — faqat bir qism uchun
<Suspense fallback={<SmallSkeleton />}>
  <SlowComponent />
</Suspense>

Amalda ikkalasini birga ishlatiladi: loading.tsx — sahifaning tez yuklanadigan qismi uchun, Suspense — sekin qismlarni alohida ajratish uchun.


Parallel ma'lumot olish

Streaming bilan birga parallel fetch ishlatish — eng tez yondashuv:

// ❌ — ketma-ket: 50 + 200 + 800 = 1050ms
export async function DashboardPage() {
  const user    = await getUser()     // 50ms
  const orders  = await getOrders()   // 200ms
  const recs    = await getRecs()     // 800ms
}
// ✅ — parallel: max(50, 200, 800) = 800ms
export async function DashboardPage() {
  const [user, orders, recs] = await Promise.all([
    getUser(),    // 50ms  ┐
    getOrders(),  // 200ms ├ bir vaqtda
    getRecs(),    // 800ms ┘
  ])
}

Lekin Promise.all da hammasi tayyor bo'lguncha kutiladi. Streaming bilan birlashtirish — har bir komponentni alohida Suspense ga o'rash — eng optimal:

export default function DashboardPage() {
  return (
    <div>
      <Suspense fallback={<Skeleton />}>
        <UserInfo />       {/* o'zi ichida fetch qiladi */}
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <RecentOrders />   {/* o'zi ichida fetch qiladi */}
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <Recommendations />{/* o'zi ichida fetch qiladi */}
      </Suspense>
    </div>
  )
}

Har bir komponent mustaqil — biri sekin bo'lsa boshqalarni to'sib qo'ymaydi.


Tez-tez uchraydigan xatolar

1. Client Component ni Suspense ga o'rash

// ❌ — Client Component da useEffect bilan fetch — Suspense kutmaydi
'use client'
export function Orders() {
  const [orders, setOrders] = useState([])
  useEffect(() => {
    fetch('/api/orders').then(r => r.json()).then(setOrders)
  }, [])
  return <ul>{orders.map(...)}</ul>
}

Suspense faqat async Server Component larni kutadi. Client Component dagi useEffect fetch — Suspense ga ko'rinmaydi.

// ✅ — async Server Component
export async function Orders() {
  const orders = await fetch('/api/orders').then(r => r.json())
  return <ul>{orders.map(...)}</ul>
}

2. Suspense ni juda yuqorida qo'yish

// ❌ — butun sahifa bitta Suspense ichida — streaming yo'q
<Suspense fallback={<Loading />}>
  <UserInfo />
  <RecentOrders />
  <Recommendations />  {/* 800ms — hammasi shu uchun kutadi */}
</Suspense>

// ✅ — har biri alohida
<Suspense fallback={<Skeleton />}><UserInfo /></Suspense>
<Suspense fallback={<Skeleton />}><RecentOrders /></Suspense>
<Suspense fallback={<Skeleton />}><Recommendations /></Suspense>

Xulosa

Streaming — sahifani bo'laklarga bo'lib yuborish. Tayyor bo'lgan qism darhol ko'rinadi, qolganlar kelguncha skeleton ko'rsatiladi.

loading.tsx   → butun sahifa uchun fallback
Suspense      → alohida komponent uchun fallback

Eng optimal:
  Har bir sekin komponent → alohida Suspense
  Har bir komponent → o'zi ichida fetch qiladi
  Parallel yuklanadi, biri sekin bo'lsa boshqalarni to'smaydi

Keyingi dars

16-dars: ISR — Incremental Static Regeneration, revalidate, on-demand

Static Rendering ning aqlli versiyasi — sahifa statik, lekin vaqti-vaqti bilan avtomatik yangilanadi.