شلال البيانات في Next.js الذي يقتل TTFB بصمت
انتظار الـ fetches واحدًا تلو الآخر داخل Server Components المتداخلة يجعل التحميل تسلسليًا — إليك كيف توازيها دون إعادة كتابة شجرة المكونات.
جعلت Server Components جلب البيانات بسيطًا من جديد: اكتب await للبيانات في المكان الذي تعرضها فيه. لكن هذه السهولة تخفي فخًا. عندما ينتظر المكوّن الأب (await) قبل أن يعرض ابنًا ينتظر بدوره، تعمل الطلبات بالتسلسل — أي شلال (waterfall) — فيتضخم زمن الوصول لأول بايت (TTFB).
اكتشاف الشلال
هذه هي النسخة التي تبدو بريئة. الصفحة تنتظر المستخدم، ثم تعرض الـ dashboard الذي ينتظر الإحصائيات:
// page.tsx — sequential, slow
export default async function Page() {
const user = await getUser(); // 200ms
return <Dashboard userId={user.id} />;
}
async function Dashboard({ userId }: { userId: string }) {
const stats = await getStats(userId); // 200ms
return <Stats data={stats} />;
}هذان الطلبان (200ms لكل منهما) لا يتداخلان — المجموع 400ms قبل أن يُرسل بايت واحد. ومع ثلاثة أو أربعة مستويات متداخلة، هكذا تتحول صفحة يفترض أن تبدو فورية إلى شاشة فارغة لمدة ثانية.
الحل 1: ارفع الـ fetches المستقلة ونفّذها معًا
إذا كان طلبان لا يعتمد أحدهما على الآخر، فابدأهما في المستوى نفسه ونفّذهما بـ Promise.all. أطلق الـ promises أولًا ثم انتظرها:
export default async function Page() {
const userPromise = getUser();
const feedPromise = getFeed();
const [user, feed] = await Promise.all([userPromise, feedPromise]);
return <Dashboard user={user} feed={feed} />;
}أصبح استدعاءان بزمن 200ms ينتهيان في نحو 200ms إجمالًا. القاعدة العملية: أي شيء لا يحتاج إلى نتيجة طلب آخر لا ينبغي أن يقف خلف await ذلك الطلب.
الحل 2: عندما تعتمد الـ fetches على بعضها، استخدم البث مع Suspense
أحيانًا تحتاج getStats فعلًا إلى معرّف المستخدم أولًا — ولا يمكن موازاة ذلك. فلا تحجب الصفحة كلها بسببه. اعرض الهيكل فورًا ودع الجزء البطيء يصل بالبث (streaming):
export default async function Page() {
const user = await getUser(); // needed for the shell
return (
<>
<Header user={user} />
<Suspense fallback={<StatsSkeleton />}>
<Stats userId={user.id} /> {/* awaits inside, streams later */}
</Suspense>
</>
);
}يظهر الـ header فورًا، وتصل الإحصائيات عندما تجهز. يشعر المستخدم بالسرعة رغم أن الـ fetch المعتمد ما زال يأخذ وقته. يحوّل Suspense الانتظار الحاجب إلى عرض تدريجي.
الحل 3: امنع التكرار حتى لا تجلب الشيء نفسه مرتين
بعد توزيع الجلب على عدة مكونات، ستستدعي getUser() في أكثر من مكان. غلّفها بـ cache من React ليعيد الطلب الواحد استخدام النتيجة خلال دورة عرض واحدة:
import { cache } from "react";
export const getUser = cache(async () => {
const res = await fetch(`${API}/me`, { next: { revalidate: 60 } });
return res.json();
});الآن يمكنك استدعاؤها بحرية في أي مكان من الشجرة — فهي تصل إلى الشبكة مرة واحدة لكل طلب.
الخلاصة
الـ await في Server Components تسلسلي افتراضيًا. وازِ البيانات المستقلة بـ Promise.all، وابثّ البيانات المعتمدة خلف Suspense، وامنع التكرار بـ cache. لا شيء من هذا يتطلب إعادة هيكلة شجرة المكونات — بل يوقف صفحتك فقط عن الانتظار لنفسها.
بقلم فادي إيهاب عامر
راسلني على واتسابتابع القراءة
التصميم بالثيمات دون خطوة بناء: كيف يستخدم NYX الدالة color-mix()
بنيت NYX، إطار مكونات بلا أي اعتماديات، بحيث يمكن إعادة ضبط ثيم كامل من حفنة custom properties — بلا Sass ولا إعادة تجميع، مباشرة في المتصفح.
ما علّمني إياه بناء OTP Input عن المكونات المتحكَّم بها
التقدم التلقائي واللصق والـ backspace تبدو بسيطة حتى تبنيها — إليك دروس الـ controlled input التي ترافقني في كل نموذج أشحنه.