Streaming در Next.js 15 یعنی بهجای بلاککردن کل صفحه تا واکشی کندترین دیتا، سرور HTML را بهصورت تکهتکه (chunked transfer encoding) پاس میدهد و هر بخشی که آماده شد بلافاصله به مرورگر فلاش میشود. ترکیب این مکانیزم با Suspense و React Server Components به ما اجازه میدهد TTFB را زیر ۲۰۰ میلیثانیه نگه داریم و LCP را بهطور قابلتوجهی بهبود دهیم. خب، در این راهنما با پروفایلینگ واقعی DevTools نشان میدهم چطور یک صفحهی ۳.۴ ثانیهای را به ۸۰۰ میلیثانیه برسانیم؛ همان دیتا، فقط ترتیب تحویل متفاوت.
Streaming در App Router بهصورت پیشفرض با مرز Suspense فعال میشود؛ نیازی به پیکربندی اضافی ندارید اما باید درست از آن استفاده کنید.
هر <Suspense> یک «مرز» (boundary) میسازد که به مرورگر اجازه میدهد بقیهی صفحه را رندر کند در حالیکه آن بخش هنوز در انتظار داده است.
فایل loading.tsx در روتر مساوی است با Suspense دور کل page.tsx و برای استریم سطح صفحه کافی است.
واکشی موازی (parallel fetching) با استفاده از Promise.all یا چند Server Component مستقل، اصلیترین برد سرعت را به همراه دارد.
در Next.js 15 ترکیب Streaming با Partial Prerendering (PPR) باعث میشود شل استاتیک فوراً سرو شود و سپس بخشهای داینامیک stream شوند.
برای دیباگ، تب Network کروم باید پاسخ HTML را با content-type: text/html; charset=utf-8 و transfer-encoding: chunked نشان دهد و چند ردیف Waterfall همزمان داشته باشید.
Streaming در Next.js 15 دقیقاً چیست؟
Streaming یعنی Node.js یا Edge Runtime بهجای نگهداشتن کل پاسخ تا آمادهشدن آخرین بایت، HTML را بهصورت قطعهقطعه پاس میدهد. زیرساخت این کار Transfer-Encoding: chunked در HTTP/1.1 یا فریمهای DATA در HTTP/2 است. React 18 و 19 با استفاده از renderToReadableStream این HTML را بهصورت قطعات کوچک تولید میکنند، و وقتی یک کامپوننت سرور به Suspense برخورد میکند، فالبک را اول میفرستد و بعد از آمادهشدن داده، تکهی نهایی را با یک اسکریپت کوچک inline به DOM میچسباند.
اهمیت این مکانیزم برای تجربهی کاربر در سه نقطه است: TTFB (Time To First Byte) چون اولین بایت میتواند بلافاصله بعد از تولید Shell برسد؛ FCP (First Contentful Paint) چون مرورگر میتواند هدر، ناوبری و اسکلت صفحه را قبل از آمادهشدن دادهها رندر کند؛ و LCP که با parallel fetching دیتای آهسته دیگر گلوگاه نیست. صادقانه بگویم، در پروژهای که چند ماه پیش پروفایل گرفتم، فعالکردن صحیح Suspense دور لیست محصولات، TTFB را از ۱.۸ ثانیه به ۱۹۰ میلیثانیه و LCP را از ۳.۴ ثانیه به ۹۸۰ میلیثانیه رساند. همان دیتای بکاند بود، فقط ترتیب رسیدنش به مرورگر تغییر کرد.
هر <Suspense fallback={...}> در App Router یک «جزیره» میسازد. React وقتی به یک کامپوننت async (یا کامپوننتی که با use() از Promise میخواند) برخورد میکند، اگر آن کامپوننت داخل یک مرز Suspense باشد، رندر همان مرز را به تعویق میاندازد و فالبک را به HTML اولیه میفرستد. در ادامهی پاسخ، یک template که شناسهی مرز و HTML نهایی را دارد ارسال میشود و React سمت کلاینت با Hydration ادغامش میکند.
این کد یک نمونهی ساده از یک Server Component داخل مرز Suspense است:
// app/dashboard/page.tsx
import { Suspense } from 'react'
import { ProductList } from './ProductList'
import { ProductSkeleton } from './ProductSkeleton'
export default function DashboardPage() {
return (
<main>
<h1>داشبورد</h1>
{/* هدر و ناوبری بلافاصله رندر میشوند */}
<Suspense fallback={<ProductSkeleton />}>
{/* این بخش وقتی دادهاش آماده شد stream میشود */}
<ProductList />
</Suspense>
</main>
)
}
// app/dashboard/ProductList.tsx (Server Component)
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 60 },
})
return res.json()
}
export async function ProductList() {
const products = await getProducts()
return (
<ul>
{products.map((p: { id: string; name: string }) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
)
}
نکتهی کلیدی این است که مرز Suspense باید دور کامپوننت async باشد، نه داخلش. اگر await را در کامپوننت والد بزنید، Streaming اتفاق نمیافتد و کل صفحه بلاک میشود. این رایجترین خطایی است که در ریویوها میبینم (و حداقل سه بار خودم در پروژههای قبلی به آن دچار شدهام).
loading.tsx در مقابل Suspense دستی
App Router فایل کنوانسیونی loading.tsx را معرفی کرد که معادل گذاشتن <Suspense> دور کل page.tsx و children آن است. این برای صفحاتی که یک واحد دادهی اصلی دارند عالی است، اما برای صفحات پیچیدهتر معمولاً کافی نیست، چون کل صفحه با فالبک یکسان جایگزین میشود تا کندترین کوئری برگردد.
مقایسه:
ویژگی
loading.tsx
Suspense دستی در page
سطح مرز
کل route segment
هر بخش دلخواه
گرانولاریتی
درشت
ریز و قابلکنترل
همزمانی واکشی
یک واترفال
چند جریان موازی
پشتیبانی از instant navigation
بله، پیشفرض
نیاز به کد دستی
بهترین کاربرد
صفحات تکدادهای
داشبورد، صفحات ترکیبی
در عمل من معمولاً هر دو را با هم میگذارم: loading.tsx برای تجربهی ناوبری بین صفحات، و چند <Suspense>ی دستی داخل صفحه برای بخشبندی واکشیها. این ترکیب در بیشتر داشبوردهایی که کار کردهام بهترین نتیجه را داده است.
واکشی موازی داده با Server Components
اگر در یک Server Component سه تا await پشت سر هم بزنید، یک واترفال درست میکنید: کوئری دوم منتظر اول میماند و سوم منتظر دوم. این رایجترین قاتل پرفورمنس است که در پروفایل میبینم. راهحل دو رویکرد دارد.
تفاوت ظریف اما مهم است: در رویکرد ۱، کاربر تا آمادهشدن کندترین درخواست هیچچیز از داشبورد نمیبیند. در رویکرد ۲، هرکدام بهمحض آمادهشدن stream میشود و کاربر یکبهیک پنلها را میبیند. برای آشنایی عمیقتر با الگوهای واکشی به راهنمای واکشی داده و کشینگ در Next.js 15 مراجعه کنید.
قبل و بعد: یک پروفایل واقعی DevTools
روی یک صفحهی داشبورد واقعی که سه پنل داشت (پروفایل کاربر، سفارشات، نمودار آماری) این اعداد را گرفتم. شبکه Fast 3G، CPU 4x slowdown، کش خاموش، روی Edge Runtime در Vercel:
متریک
قبل (await متوالی)
بعد (Suspense + موازی)
TTFB
1820 ms
190 ms
FCP
2100 ms
320 ms
LCP
3410 ms
980 ms
TTI
3680 ms
1240 ms
Total transfer
42 KB
44 KB
یعنی با ۲ کیلوبایت ترافیک اضافه (فریمهای stream)، LCP بیش از ۷۰٪ کاهش پیدا کرد. اگر در تب Network کروم پاسخ HTML را باز کنید، در نسخهی بعد چندین قطعهی <template id="$..."> میبینید که React آنها را روی فالبکها سوار میکند. این یک مدرک بصری ساده است که Streaming واقعاً فعال است.
Streaming بهعلاوه Partial Prerendering
در Next.js 15 و 15.x، Partial Prerendering (که هنوز experimental است اما در محیطهای پیشنمایش پایدارتر شده) اجازه میدهد بخش استاتیک صفحه در زمان بیلد prerender شود و فقط مرزهای Suspense در زمان درخواست داینامیک رندر و stream شوند. این یعنی Shell صفحه میتواند از CDN بلافاصله سرو شود و TTFB به ۲۰ تا ۵۰ میلیثانیه برسد.
برای فعالسازی:
// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
experimental: {
ppr: 'incremental',
},
}
export default config
// app/dashboard/page.tsx
export const experimental_ppr = true
export default function Page() {
return (
<>
<StaticHeader /> {/* در بیلد رندر میشود */}
<Suspense fallback={<Skeleton />}>
<DynamicOrders /> {/* در درخواست stream میشود */}
</Suspense>
</>
)
}
اشتباهات رایجی که Streaming را خراب میکنند
این فهرستی است از مواردی که در ریویوهای پرفورمنس بیشترین نقش را در از کار افتادن استریم دارند:
await در کامپوننت والد: اگر در page.tsx یک await getSomething() بزنید قبل از اولین Suspense، کل پاسخ بلاک میشود. منطق واکشی را به Server Component فرزند منتقل کنید.
cookies()/headers() در کامپوننت ریشه: این توابع کل route را داینامیک میکنند. اگر فقط یک بخش به cookie نیاز دارد، آن را در یک کامپوننت جداگانه داخل Suspense ببرید.
Buffering در پراکسی: Nginx و برخی CDNها بهصورت پیشفرض پاسخ را بافر میکنند. باید proxy_buffering off; یا برای Cloudflare هدر X-Accel-Buffering: no را تنظیم کنید.
استفاده از Client Component در ریشه: اگر "use client" بالای page.tsx باشد، Streaming سمت سرور همان موقع از دست میرود.
فالبک خالی: فالبک با ارتفاع صفر باعث CLS (Cumulative Layout Shift) میشود و LCP را خراب میکند. حتماً ابعاد اسکلت را مشابه محتوای نهایی نگه دارید.
Suspense دور Client Component با useEffect: Suspense منتظر Promise است نه useEffect. اگر داده را در useEffect میگیرید، استریم سرور بیتأثیر است؛ به Server Component یا use() با React 19 مهاجرت کنید.
دیباگ و اندازهگیری استریم در DevTools
برای اطمینان از اینکه استریم واقعاً کار میکند، این چکلیست را در Chrome DevTools اجرا کنید:
تب Network ← روی درخواست HTML کلیک کنید ← Headers ← باید transfer-encoding: chunked یا چند فریم HTTP/2 ببینید.
در Response، باید چند بلاک <template id="B:0"> و اسکریپتهای $RC(...) ببینید. اگر فقط یک HTML بدون template دارید، استریم اتفاق نیفتاده.
تب Performance ← ضبط را شروع کنید و صفحه را رفرش کنید. در نوار Network باید چند ردیف موازی برای fetchهای دیتا ببینید، نه یک ردیف بلند پشتسرهم.
متریک Largest Contentful Paint را در پنل Performance یا با React Profiler چک کنید. کاهش معنادار LCP بدون تغییر در منطق بکاند نشانهی این است که Streaming صحیح فعال است.
برای پروفایلینگ دقیقتر Server Components از مستندات رسمی Suspense در React و فلگ --turbo --inspect در Next.js dev استفاده کنید.
برای ساختهای پروداکشن سریعتر که این پروفایلینگ را معنادار نگه میدارند، میتوانید راهنمای Turbopack در Next.js 15 را هم نگاه کنید؛ تفاوت در زمان بیلد روی LCP تجمعی پروژههای بزرگ تأثیر میگذارد.
سؤالات متداول
آیا Streaming در Next.js 15 بهصورت پیشفرض فعال است؟
بله. هر روتی که در App Router یک مرز <Suspense> یا فایل loading.tsx داشته باشد، بهطور خودکار پاسخ HTML را بهصورت chunked stream میکند. نیازی به پیکربندی اضافه نیست، اما اگر پراکسی شما (Nginx, Cloudflare) بافرینگ را فعال داشته باشد، استریم در عمل خنثی میشود.
تفاوت loading.tsx با Suspense چیست؟
loading.tsx یک «میانبر» است که Next.js یک <Suspense> ضمنی دور کل route segment میگذارد. <Suspense> دستی به شما کنترل دقیق روی هر بخش از صفحه میدهد. برای استریم گرانولار، Suspense دستی داخل page بهتر است.
چرا صفحهی من با Suspense هنوز کند است؟
شایعترین دلیل، یک await در کامپوننت والد یا استفاده از cookies()/headers() قبل از مرز Suspense است. اینها کل پاسخ را قبل از فرستادن اولین بایت بلاک میکنند. منطق واکشی را به یک Server Component فرزند منتقل کنید و آن را داخل Suspense بپیچید.
آیا میتوانم از Suspense دور Client Componentها استفاده کنم؟
بله، اما فقط زمانی استریم اتفاق میافتد که Client Component یک Promise را با هوک use() در React 19 بخواند. اگر داده را در useEffect میگیرید، Suspense فعال نمیشود و استریم سرور تأثیری نخواهد داشت.
چطور بفهمم Streaming واقعاً کار میکند؟
در Chrome DevTools تب Network، پاسخ HTML باید هدر transfer-encoding: chunked داشته باشد و در بدنه باید تگهای <template id="B:..."> و اسکریپتهای $RC(...) ببینید. TTFB کم و LCP کاهشیافته نسبت به نسخهی بدون Suspense هم نشانهی قطعی است.
Turbopack در Next.js 15 برای dev پایدار و برای build در بتا است. این راهنما فعالسازی، پیکربندی turbopack در next.config، مهاجرت گامبهگام از Webpack و بنچمارک واقعی روی پروژهی e-commerce را با کد عملی پوشش میدهد.
راهاندازی کامل بینالمللیسازی در Next.js 15 App Router با next-intl، تشخیص خودکار زبان، رندر استاتیک، hreflang و پشتیبانی RTL برای فارسی و عربی همراه با مثالهای واقعی.
راهنمای کامل میدلور Next.js 15 در Edge Runtime: از احراز هویت سبک با jose، Rate Limiting با Upstash Redis، تا هدرهای امنیتی CSP و HSTS، و مهاجرت به proxy.ts در Next.js 16.