Menu
flag Ar iconالعربيةdown icon

Suspense وlazy في React: حالات التحميل وتقسيم الكود

يعرض Suspense بديلًا ما دامت المكوّنات بداخله غير جاهزة، ويحمّل React.lazy كود المكوّن فقط عند عرضه أول مرة. تعلّم تقسيم الكود، والحدود المتداخلة، وSuspense مع use() في React 19، والتعامل مع الأخطاء.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

يعرض Suspense بديلًا، مثل رسالة تحميل، ما دامت المكوّنات بداخله غير جاهزة بعد. وReact.lazy أشيع ما يُنتظر: يحمّل كود المكوّن فقط عندما يُعرض ذلك المكوّن أول مرة، ما يُبقي التنزيل الأولي صغيرًا.

انقر Show chart: يظهر Loading chart... لثانية، ثم الرسم البياني. أخفه وأظهره مرة أخرى فيظهر فورًا دون سطر سجل جديد، لأن lazy تحتفظ بالوحدة المحمّلة.

تقسيم الكود باستخدام lazy

يحتوي المحرر على كل شيء في ملف واحد، لذا يبني المثال الوحدة البطيئة يدويًا: وعد يُحل بعد ثانية إلى كائن فيه تصدير default. أما في تطبيق حقيقي فيعيش المكوّن في ملفه الخاص وتمرر استيرادًا ديناميكيًا:

import { lazy, Suspense } from 'react';

const Chart = lazy(() => import('./Chart.jsx'));

export default function Dashboard() {
    return (
        <Suspense fallback={<p>Loading chart...</p>}>
            <Chart />
        </Suspense>
    );
}

يُرجع import('./Chart.jsx') وعدًا بالوحدة. ترى أدوات التجميع مثل Vite وwebpack الاستيراد الديناميكي وتضع Chart.jsx وكل ما لا يستخدمه إلا هو في ملف منفصل، يُنزَّل في أول مرة يُعرض فيها <Chart />. قواعد ينبغي معرفتها:

  • تحتاج الوحدة إلى تصدير افتراضي. تقرأ lazy الخاصية default مما يُحل إليه الوعد. ولتصدير مسمى، حوّله: lazy(() => import('./charts.js').then((m) => ({ default: m.LineChart }))).
  • استدعِ lazy في المستوى الأعلى من الوحدة. داخل مكوّن ستنشئ نوع مكوّن جديدًا في كل عرض، فتزيل React القديم، وتفقد حالته وتحمّله من جديد.
  • قسّم حيث ينتظر المستخدم على أي حال. المسارات والنوافذ المنبثقة واللوحات النادرة الفتح والعناصر الثقيلة (المحررات والرسوم البيانية والخرائط) مرشحة جيدة. أما تقسيم كل مكوّن صغير فيضيف طلبات وحالات تحميل دون فائدة.

التحميل المسبق قبل النقر

يبدأ المكوّن الكسول التحميل عندما يُعرض أول مرة، فينتظر المستخدم دائمًا تنزيلًا واحدًا على الأقل بعد النقر. إذا استطعت توقع أن النقر قادم، فابدأ التنزيل مبكرًا. احتفظ بدالة الاستيراد في متغير واستدعِها عند التمرير أو التركيز؛ يحتفظ المتصفح بالوحدة، فعندما تستدعي lazy الاستيراد نفسه لاحقًا، يُحل دون تنزيل ثانٍ.

const loadChart = () => import('./Chart.jsx');
const Chart = lazy(loadChart);

<button onMouseEnter={loadChart} onFocus={loadChart} onClick={() => setShow(true)}>
    Show chart
</button>

كيف يقرر Suspense ما يعرضه

عندما لا يكون مكوّن داخل <Suspense> جاهزًا، فإنه يُعلَّق: توقف React عرض ذلك الجزء وتعرض fallback لأقرب Suspense فوقه. كل ما داخل تلك الحدود يُستبدل بالبديل، لا المكوّن المنتظر وحده. وعندما يجهز ما كان ينتظره، تعرض React المحتوى من جديد وتستبدل البديل.

وهذا يجعل موضع الحدود قرار تصميم. ضع الأجزاء المستقلة في حدودها الخاصة ليظهر كل منها عندما يجهز:

أولًا تعرض الصفحة كلها Loading page...، لأن Header ينتمي إلى الحدود الخارجية. وبمجرد أن تجهز الترويسة، يظهر المقال وتحته Loading comments...، وتصل التعليقات أخيرًا. غيّر 1500 إلى 3000 فتُعاد تحميل المعاينة بانتظار أطول للتعليقات وحدها. احذف <Suspense> الداخلية (واحتفظ بـ <Comments />) فتنتظر الصفحة التعليقات قبل أن تعرض أي شيء.

Suspense مع use() في React 19

في React 19 يمكن للمكوّن قراءة وعد بـ use(promise). إذا كان الوعد ما زال معلقًا، يُعلَّق المكوّن ويعرض أقرب Suspense بديله؛ وعندما يُحل، تُرجع use القيمة. تتناولها صفحة الخطاف use بالكامل. الدالة الوهمية fetchUser أدناه بديل عن طلب حقيقي.

تنقّل بين المستخدمين، ثم عد إلى User 1: يظهر فورًا ولا تسجّل Console جلبًا جديدًا، لأن الوعد موجود أصلًا في الذاكرة المؤقتة.

لماذا يجب تخزين الوعد مؤقتًا

الخريطة cache ليست تحسينًا هنا، بل هي مطلوبة. المكوّن الذي يُعلَّق لا يحتفظ بأي شيء من تلك المحاولة: عندما يُحل الوعد، تعرضه React من جديد من البداية. لو استدعى Profile الدالة fetchUser(id) مباشرة، لأنشأت كل محاولة وعدًا جديدًا، وبدأت طلبًا جديدًا وعُلّقت عليه من جديد. لا يظهر الملف الشخصي أبدًا، وتمتلئ Console بأسطر fetching user. لذا يجب أن يأتي الوعد من مكان يعيش أطول من العرض:

  • ذاكرة مؤقتة مفتاحها الطلب، مثل الخريطة أعلاه (تفعل مكتبات البيانات مثل TanStack Query ومحمّلات أطر العمل ذلك نيابة عنك)؛
  • أب ينشئ الوعد مرة واحدة، في معالج حدث أو مكوّن خادم، ويمرره إلى الأسفل كـ prop.

لاحظ أن كل مستخدم جديد ما زال يستبدل الملف الشخصي بالبديل. إذا كنت تفضّل إبقاء المستخدم القديم على الشاشة حتى يجهز التالي، فغلّف التحديث في انتقال: startTransition(() => setId(n)). لا تخفي React المحتوى الظاهر أصلًا في حالة الانتقال (راجع useTransition).

الأخطاء تحتاج إلى حدود أخطاء

يتعامل Suspense مع الانتظار، لا مع الفشل. إذا فشل استيراد كسول (انقطع اتصال المستخدم، أو أزال نشر جديد الجزء القديم) أو رُفض وعد مُرّر إلى use، ترمي React الخطأ إلى أقرب حدود أخطاء. ومن دونها تُزال الشجرة كلها تحت الجذر. وما زالت حدود الأخطاء مكوّنات أصناف (راجع حدود الأخطاء):

<ErrorBoundary fallback={<p>Could not load the chart.</p>}>
    <Suspense fallback={<p>Loading chart...</p>}>
        <Chart />
    </Suspense>
</ErrorBoundary>

ما لا يكتشفه Suspense

لا يتفاعل Suspense إلا مع المكوّنات التي تُعلَّق: مكوّنات lazy، وuse(promise)، ومصادر البيانات المبنية لـ Suspense (محمّلات أطر العمل، والمكتبات الداعمة لـ Suspense). أما الجلب داخل useEffect الذي يضبط الحالة عند اكتماله فلا يُعلِّق، فلا تعرض حدود Suspense حوله بديلها أبدًا. ولهذا النمط تحتفظ بحالة loading خاصة بك، كما يظهر في صفحة جلب البيانات.

الأسئلة الشائعة

ما هو Suspense في React؟

<Suspense fallback={...}> مكوّن يعرض fallback الخاص به ما دام أي مكوّن بداخله ينتظر شيئًا، مثل كود يُحمَّل بشكل كسول أو بيانات تُقرأ بـ use. وعندما يجهز كل ما بداخله، تستبدل React البديل بالمحتوى.

ماذا يفعل React.lazy؟

ينشئ lazy(() => import('./Chart.jsx')) مكوّنًا يُنزَّل كوده في أول مرة يُعرض فيها. وتضع أدوات التجميع ذلك الملف في جزء منفصل، فتحمّل الصفحة الأولى قدرًا أقل من JavaScript.

هل يعمل Suspense مع جلب البيانات؟

نعم، عندما يدعمه مصدر البيانات. في React 19 يمكن للمكوّن قراءة وعد بـ use(promise) والتعليق حتى يُحل. وتدمج أطر العمل مثل Next.js أيضًا Suspense مع تحميل بياناتها. أما الجلب داخل useEffect فلا يطلق Suspense.

كيف أتعامل مع الأخطاء مع Suspense؟

لا يتعامل Suspense إلا مع الانتظار. إذا فشل استيراد كسول أو وعد، يذهب الخطأ إلى أقرب حدود أخطاء، فغلّف حدود Suspense (أو أباها) بواحدة.

أين أستدعي lazy؟

في المستوى الأعلى من الوحدة، خارج أي مكوّن. استدعاء lazy داخل مكوّن ينشئ نوع مكوّن جديدًا في كل عرض، ما يعيد ضبط حالته ويعيد تحميله.

رسم توضيحي للغات البرمجة في Coddy

تعلّم البرمجة مع Coddy

ابدأ الآن