ข้ามไปยังเนื้อหา

Streaming และ Suspense

streaming ให้ server ส่งส่วนที่พร้อมแล้วของหน้า — คือ static shell — ออกไปทันที แล้วค่อยดันส่วนที่ช้ากว่าตามมาทาง connection เดียวกันเมื่อ render เสร็จ

ถ้าไม่มี streaming server จะกั๊ก response ทั้งก้อนไว้จนกว่าทุก await จะ resolve ดังนั้นชิ้นที่ data ช้าที่สุดจะเป็นตัวกำหนด time to first byte streaming ตัดหน้าออกเป็น chunk — shell (layout, heading, อะไรก็ตามที่ไม่รอ data) ออกจาก server ก่อน ส่วนที่ช้าตามมาเมื่อ resolve

browser paint shell ทันที ผู้ใช้จึงเห็นหน้าจริงในขณะที่ data ยังโหลดอยู่เบื้องหลัง

วาง loading.tsx ไว้ข้าง ๆ page.tsx แล้ว Next.js จะห่อทั้ง segment นั้นด้วย Suspense boundary ให้เอง ระหว่างที่ page รอ data อยู่ loading UI จะโชว์ทันที

// app/feed/loading.tsx — shown instantly while page.tsx awaits its data
export default function Loading() {
return <p>Loading your feed…</p>;
}

คุณไม่ต้องเขียน <Suspense> เองตรงนี้ — file convention คือ boundary เอง segment จะ stream เข้ามาเมื่อ data พร้อม

เมื่อมีแค่ region เดียวของหน้าที่ช้า ให้ห่อ region นั้นด้วย <Suspense> ส่วนที่เหลือของหน้าจะ render ทันที และเฉพาะ component ที่ช้าเท่านั้นที่ stream เข้ามา

app/feed/page.tsx
import { Suspense } from 'react';
async function SlowComments() {
const comments = await fetch('https://api.example.com/comments', {
cache: 'no-store',
}).then((r) => r.json());
return <ul>{comments.map((c) => <li key={c.id}>{c.text}</li>)}</ul>;
}
export default function FeedPage() {
return (
<section>
<h1>Your feed</h1>
<p>This heading and text ship in the shell, immediately.</p>
<Suspense fallback={<p>Loading comments…</p>}>
<SlowComments />
</Suspense>
</section>
);
}

heading paint ทันที ส่วน SlowComments stream เข้ามาแล้วแทนที่ fallback เมื่อ fetch resolve

streaming แยก “first paint” ออกจาก “data ที่ช้าที่สุด” มีสองข้อดีที่ตามมา —

  • TTFB ดีขึ้น — shell ออกจาก server โดยไม่รอ data request ใด ๆ byte แรกจึงมาถึงเร็ว
  • perceived performance ดีขึ้น — ผู้ใช้อ่าน layout จริงได้ระหว่างที่ region ช้ากำลังเติมเข้ามา แทนที่จะจ้องหน้าจอว่างจนกว่าทุกอย่างจะพร้อม
flowchart LR
  A[Request] --> B[Server sends static shell now]
  B --> C[Browser paints shell, fast TTFB]
  D[Slow async component resolves] --> E[Server streams the chunk]
  E --> F[Suspense fallback swaps to real content]
Shell first, then stream the slow parts
อะไรถูกส่งไปที่ browser ก่อนเมื่อหน้า stream?
ไฟล์ loading.tsx สร้างอะไรให้ segment ของตัวเอง?
ควรใช้ <Suspense> แบบ manual เมื่อไหร่?
ทำไม streaming ช่วยเรื่อง time to first byte?