React use() وSuspense: بثّ البيانات بدون شلالات
React use() وSuspense: بثّ البيانات بدون شلالات
للشلال خاصية حسابية قبيحة. أربعة طلبات بـ300ms في سلسلة تعني 1.2 ثانية من الشاشة الفارغة لبيانات كان يمكن أن تصل في 300ms.
ما يفعله المكوّن المُعلَّق فعلاً
المكوّن الذي ليس جاهزاً يرمي Promise. ليس خطأً، بل Promise. يمسكه React كما تمسك Error Boundary الأخطاء، يتجاهل العمل المُقدَّم جزئياً، يعرض أقرب احتياطي ويشترك في الـPromise المرمي. عندما يُحلّ، يُعيد React تشغيل المكوّن من البداية.
يجب أن يكون الـPromise مستقراً بين هذه التشغيلات. Promise جديد في كل تشغيل يعني تعليقاً لا نهاية له. هذه هي أكثر طريقة شائعة لإفساد use().
tsx
"use client";import { use } from "react";export default function Posts({ posts }) {// يُعلَّق حتى يُحلّ الـpromiseconst allPosts = use(posts);return (<ul>{allPosts.map((post) => (<li key={post.id}>{post.title}</li>))}</ul>);}
Suspense والغلاف الثابت وHTML يصل بترتيب خاطئ
يُرسل React الغلاف الثابت فوراً. تُضاف القطعة المتأخرة إلى نهاية المستند، خارج نطاق الرؤية، ويبثّ React <script> مدمجاً صغيراً بجانبها لنقل المحتوى إلى مكانه الصحيح.
tsx
import { Suspense } from "react";export default function Page() {return (<main><h1>لوحة التحكم</h1> {/* يُرسل فوراً */}<Suspense fallback={<StatsSkeleton />}><Stats /></Suspense><Suspense fallback={<FeedSkeleton />}><Feed /></Suspense></main>);}
ابدأ مبكراً، اقرأ متأخراً
tsx
import { Suspense } from "react";import Posts from "@/app/ui/posts";import Comments from "@/app/ui/comments";export default function Page() {// كلا الطلبين يبدآن الآن، بالتوازيconst posts = getPosts();const comments = getComments();return (<><Suspense fallback={<div>جارٍ تحميل المقالات...</div>}><Posts posts={posts} /></Suspense><Suspense fallback={<div>جارٍ تحميل التعليقات...</div>}><Comments comments={comments} /></Suspense></>);}
الفخ الذي لا يتوقعه أحد: البثّ يستخدم كود حالة HTTP الخاص بك
عندما يُقدَّم الاحتياطي Suspense ويفتح البثّ، يكون الخادم قد التزم بالفعل بـ200 OK. لا يمكن لـnotFound() الذي يُشغَّل داخل حد مبثوث أن يعطي 404 حقيقياً.
tsx
export default async function PostPage({ params }) {const { slug } = await params;const exists = await checkSlugExists(slug); // سريع، قبل فتح البثّif (!exists) notFound(); // 404 حقيقيreturn (<Suspense fallback={<PostSkeleton />}><PostContent slug={slug} /></Suspense>);}
ما يفعله البثّ بـCore Web Vitals
- ينخفض TTFB إلى حوالي وقت تقديم التخطيط.
- قد يتدهور LCP، لا يتحسّن. احتفظ بعناصر LCP في الغلاف الثابت.
- CLS مسؤوليتك.
- يتحسّن INP بفضل الترطيب الانتقائي.
المزالق وأفضل الممارسات
- لا تُنشئ أبداً Promise أثناء تقديم Client Component.
- Suspense يمسك الانتظار، لا الأخطاء. اقرن كل حدّ بـError Boundary.
- حجّم الحدود كما يُدركها الإنسان.
- الروبوتات لا تستقبل البثّ.
- خاصية Promise تعبر الشبكة.
خلاصة
بدلاً من تسلسل — طلب، انتظار، تقديم، طلب، انتظار، تقديم — تحصل على بثوث متوازية: كل شيء يبدأ في وقت واحد، وتظهر كل جزء من الصفحة فور وصول بياناتها الخاصة.