Fady.
كل المقالات
قراءة في 6 دقائق

شلال البيانات في Next.js الذي يقتل TTFB بصمت

انتظار الـ fetches واحدًا تلو الآخر داخل Server Components المتداخلة يجعل التحميل تسلسليًا — إليك كيف توازيها دون إعادة كتابة شجرة المكونات.

Next.jsReactPerformance

جعلت Server Components جلب البيانات بسيطًا من جديد: اكتب await للبيانات في المكان الذي تعرضها فيه. لكن هذه السهولة تخفي فخًا. عندما ينتظر المكوّن الأب (await) قبل أن يعرض ابنًا ينتظر بدوره، تعمل الطلبات بالتسلسل — أي شلال (waterfall) — فيتضخم زمن الوصول لأول بايت (TTFB).

اكتشاف الشلال

هذه هي النسخة التي تبدو بريئة. الصفحة تنتظر المستخدم، ثم تعرض الـ dashboard الذي ينتظر الإحصائيات:

tsx
// 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 أولًا ثم انتظرها:

tsx
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):

tsx
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 ليعيد الطلب الواحد استخدام النتيجة خلال دورة عرض واحدة:

tsx
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. لا شيء من هذا يتطلب إعادة هيكلة شجرة المكونات — بل يوقف صفحتك فقط عن الانتظار لنفسها.


بقلم فادي إيهاب عامر

راسلني على واتساب

تابع القراءة