Streaming: ارسال تدریجی صفحه به مرورگر

در درس قبل دیدیم که دریافت داده در Server Component ساده است، ولی یک پرسش باقی ماند: اگر یکی از درخواست‌های داده کند باشد، کاربر تا کی باید به یک صفحه خالی نگاه کند؟ پاسخ این درس Streaming است: سرور لازم نیست صبر کند تا کل صفحه آماده شود؛ می‌تواند هر بخش را به‌محض آماده شدن بفرستد.

مشکل: ارسال همه‌چیز یا هیچ‌چیز

فرض کنید صفحه‌ای شامل یک هدر سریع، یک مقاله که ۲۰۰ میلی‌ثانیه طول می‌کشد، و یک بخش نظرات که ۳ ثانیه طول می‌کشد داریم. اگر سرور صبر کند تا همه‌چیز آماده شود و بعد یک‌جا بفرستد، کاربر هیچ چیزی را زودتر از ۳ ثانیه نمی‌بیند؛ حتی بخش‌هایی که از ثانیه‌های اول آماده بودند.

راه‌حل: Suspense به‌عنوان مرز بخش‌های مستقل

مرزهای Suspense که در فصل بیست‌ویکم دیدیم، دقیقاً همین نقش را بازی می‌کنند: مشخص می‌کنند کدام بخش‌ها می‌توانند مستقل از بقیه و دیرتر برسند.

jsx
import { Suspense } from "react";

export default function Page() {
  return (
    <>
      <Header />

      <Suspense fallback={<PostSkeleton />}>
        <Post />
      </Suspense>

      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />
      </Suspense>
    </>
  );
}

در اینجا Post و Comments می‌توانند Server Component از نوع async باشند. سرور ابتدا همه چیزی را که آماده است، یعنی هدر و fallbackها، می‌فرستد. بعد، هر مرز Suspense به‌محض آماده شدن، محتوای واقعی خود را می‌فرستد و جای fallback را می‌گیرد.

خط زمانی ارسال

html
زمان ۰ ms    هدر + اسکلت مقاله + اسکلت نظرات ارسال می‌شود (Shell)
زمان ۲۰۰ ms  محتوای مقاله می‌رسد و جای اسکلت را می‌گیرد
زمان ۳۰۰۰ ms محتوای نظرات می‌رسد و جای اسکلت را می‌گیرد

به بخش اولیه که پیش از آماده شدن داده‌های کند ارسال می‌شود، Shell می‌گویند. کاربر Shell را بلافاصله می‌بیند و مقاله را بعد از ۲۰۰ میلی‌ثانیه، نه ۳ ثانیه.

ارتباط با Promise به‌عنوان Prop

الگوی درس ۲۴-۶، یعنی شروع یک درخواست در Server Component و دادن Promise آن به یک Client Component که با use آن را می‌خواند، در عمل همین Streaming را ممکن می‌کند. Promise حل‌نشده Prop است، Client Component آن را می‌خواند، Suspend می‌شود، و نتیجه وقتی آماده شد به مرورگر فرستاده می‌شود.

چیزی که بدون Suspense رخ می‌دهد

اگر یک کامپوننت کند، داخل هیچ مرز Suspense ای نباشد، نمی‌تواند جدا از بقیه ارسال شود. در آن حالت، خروجی بیرون از نزدیک‌ترین مرز Suspense (یا کل صفحه، اگر هیچ مرزی نباشد) تا آماده شدن آن کامپوننت منتظر می‌ماند. پس Streaming خودکار نیست؛ ما با کشیدن مرزهای Suspense تعیین می‌کنیم چه بخش‌هایی مستقل بمانند.

انتخاب جای مرز

تصمیم‌گیری درباره دانه‌بندی مرزها همان Trade-off درس ۲۱-۳ است: یک مرز بزرگ، ساده است و همه چیز را با هم نشان می‌دهد؛ چند مرز کوچک، تجربه تدریجی‌تری می‌دهد ولی صفحه موقع بارگذاری شلوغ‌تر و پراکنده‌تر می‌شود. چند نکته مکمل:

  • محتوای مهم‌تر و سریع‌تر را بیرون از مرزهای کند نگه دارید تا در Shell بیاید.
  • اندازه fallback را نزدیک اندازه محتوای واقعی بگیرید تا صفحه بعد از رسیدن محتوا نپرد (درس ۲۱-۳).
  • بخش‌های مستقل را در مرزهای جدا بگذارید، تا کندی یکی جلوی بقیه را نگیرد.

Hydration هم بخش‌به‌بخش می‌شود

Streaming فقط درباره ارسال HTML نیست. طبق چیزی که در درس ۲۴-۵ دیدیم، Client Componentها باید Hydrate شوند. با مرزهای Suspense، React می‌تواند این کار را هم بخش‌به‌بخش و به‌محض رسیدن کد هر بخش انجام دهد، و اگر کاربر روی بخشی که هنوز Hydrate نشده کلیک کند، آن بخش را با اولویت بیشتری Hydrate کند. این همان ایده اولویت‌بندی Scheduler فصل بیست‌ودوم است.

محدودیت: وضعیت پاسخ HTTP

وقتی سرور شروع به فرستادن Shell کرد، کد وضعیت پاسخ (مثلاً ۲۰۰) از پیش ارسال شده و دیگر قابل تغییر نیست. پس تصمیم‌هایی مثل «این صفحه وجود ندارد» (۴۰۴) یا «کاربر باید به جای دیگری برود» (Redirect) باید پیش از شروع Streaming گرفته شوند. اینکه یک Framework این موضوع را چگونه مدیریت می‌کند، به خود Framework بستگی دارد.

چه کسی Streaming را راه می‌اندازد

پشت صحنه، React APIهایی در بسته react-dom/server برای رندر به‌صورت جریان (Stream) ارائه می‌دهد. اما در عمل، تقریباً هیچ‌کس مستقیماً این‌ها را صدا نمی‌زند؛ Frameworkهای پشتیبان Server Components این کار را خودکار انجام می‌دهند و ما فقط با مرزهای Suspense تعیین می‌کنیم چه چیزی مستقل باشد. نقش Frameworkها در درس بعدی بررسی می‌شود.

چرا این درس مثال قابل اجرا ندارد

Streaming واقعی به یک سرور و Framework با پشتیبانی از Server Components نیاز دارد که در ویرایشگر درون‌صفحه‌ای وجود ندارد. کدها را در یک پروژه واقعی اجرا کنید و در ابزار توسعه‌دهنده مرورگر، زمان رسیدن بخش‌های مختلف را ببینید.

جمع‌بندی

Streaming یعنی سرور هر بخش از صفحه را به‌محض آماده شدن بفرستد، نه اینکه صبر کند کل صفحه تمام شود. مرزهای Suspense مشخص می‌کنند کدام بخش‌ها مستقل‌اند: بخش اولیه (Shell) همراه با fallbackها بلافاصله می‌رسد و بقیه بعداً جایگزین می‌شوند. بدون مرز Suspense، یک کامپوننت کند همه‌چیز را منتظر نگه می‌دارد. Hydration هم بخش‌به‌بخش و با اولویت تعامل کاربر انجام می‌شود. پس از شروع ارسال، کد وضعیت HTTP قابل تغییر نیست. در درس بعدی، به نقش Frameworkها در Server Components می‌پردازیم.