**MSA Title:** أفضل Loading States هي No Loading States
Published: 29 May 2026
غالبًا ما تنتهي التطبيقات بهياكل عظمية، ودوارات تحميل، وتأثيرات تلميع، وطبقات تحميل، وبدائل suspense، وجميع أنواع واجهات المستخدم الأخرى التي هدفها الوحيد هو شغل المساحة التي يجب أن تظهر فيها البيانات في النهاية.
نقضي جميعًا وقتًا مفاجئًا في حل نفس المشكلة، ولا شيء من ذلك يُعد عملًا منتجًا فعليًا، لذا لم أستطع التوقف عن التفكير في كيفية تجنّب حالات التحميل تمامًا.
عند النظر إلى تاريخ الويب، أعتقد أننا كنا نمتلك إجابة جيدة بالفعل. لقد نسيناها لفترة.
The Web Already Had an Answer
إذا كنت تبني مواقع ويب منذ ما قبل تطبيقات الصفحة الواحدة (SPAs)، فستتذكر أن حالات التحميل لم تكن موجودة في كل مكان.
عندما ينقر المستخدم على رابط، يرسل المتصفح طلبًا، ينتظر استجابة الخادم، ثم يعرض الصفحة التالية. كان المتصفح يتولى الانتظار، مما يعني أن المستخدمين لم ينتقلوا إلى صفحات نصف مُعالجة لأنه لا وجود لصفحة نصف مُعالجة. إما أن الصفحة التالية لم تكن جاهزة بعد، أو كانت جاهزة.
لم يكن هذا النموذج مثاليًا، لكنه كان يحمل فائدة جميلة جدًا… كان التحميل يُعالج على مستوى التطبيق بدلاً من مستوى المكوّن. لم نكن بحاجة إلى صيانة حالات تحميل متناثرة في قاعدة الشيفرة لأن المتصفح كان ينسق ذلك بالفعل.
ثم ظهرت تطبيقات الصفحة الواحدة (SPAs) وأصبح التنقل فوريًا، مما شعرنا به كتحسين هائل. بدلاً من الانتظار لوصول صفحة جديدة، يمكننا تبديل المسارات فورًا والبدء في العرض على الفور.
المقابل هو أننا غالبًا ما نتنقل قبل أن تكون البيانات جاهزة، وفي تلك اللحظة نواجه مشكلة جديدة. كيف نملأ الفراغ الفارغ أثناء تحميل البيانات؟
أصبح الجواب عبارة عن هياكل عظمية، دوارات تحميل، تأثيرات تلميع، حالات احتياطية للتشويق، طبقات تحميل، وعدد لا يحصى من المتغيرات لنفس الفكرة. لقد استبدلنا شكلًا واحدًا من الانتظار بآخر.
توقفنا عن الانتظار قبل التنقل وبدأنا الانتظار بعد التنقل.
انتقالات المسارات (لإنقاذ الوضع؟)
ما أجده مثيرًا للاهتمام في انتقالات المسارات هو أنها تسمح لنا بالاقتراب من نموذج الويب الأصلي. ليس بالكامل، لأننا لا نزال نحصل على جميع مزايا التنقل من جانب العميل، لكن النموذج الذهني يصبح مشابهًا بشكل مدهش.
عندما أقول “انتقالات المسارات”، لا أتحدث عن الرسوم المتحركة. أنا أتحدث عن قدرة الموجه (router) على بدء تنقل، تحميل البيانات في الخلفية، وتأخير تنفيذ تغيير المسار حتى تكون كل ما يلزم للشاشة التالية جاهزًا.
بدلاً من التنقل، ثم العرض، ثم جلب البيانات، ثم ملء الصفحة تدريجيًا مع وصول تلك البيانات، يمكن للنقر على رابط أن يبدأ بتحميل البيانات، ينتظرها، ثم ينتقل إلى صفحة مكتملة.
للوهلة الأولى يبدو ذلك خطوة إلى الوراء. فكان هناك سبب وجيه لنقل التحميل إلى المكونات في البداية… الأداء المتصور.
إذا نقر المستخدم على رابط واستجاب التطبيق فورًا، فإن ذلك يشعره بالسرعة لأن شيئًا ما حدث. يحصل المستخدم على رد فعل ويظهر التطبيق استجابة.
هذا هدف معقول، لكن المستخدمين غالبًا ما ينتظرون بنفس المدة. لقد نقلنا الانتظار فقط دون إزالته.
توفر لنا انتقالات المسارات خيارًا آخر. يمكننا بدء التحميل قبل أن ينقر المستخدم، الانتظار فقط عندما نحتاج فعلًا إلى ذلك، وتجنب إضافة هياكل عظمية وحالات تحميل عبر تطبيقنا، دون جعل التطبيق يبدو أبطأ.
أنا أستخدم أمثلة TanStack Router طوال هذا المقال لأن واجهات التحميل والتحميل المسبق تجعل النمط سهلًا للعرض، لكن الفكرة الأساسية تنطبق على أي حل توجيه يدعم انتقالات مسارات React.
Preload Everything
المفتاح لجعل هذا يعمل هو التحميل المسبق.
إذا كان من المحتمل أن يحتاج المستخدم إلى بعض البيانات قريبًا، ابدأ بتحميلها قبل أن يطلبها. تمرير المؤشر فوق رابط أو دخول رابط إلى نافذة العرض كلاهما فرص لبدء تحميل البيانات مسبقًا.
باستخدام TanStack Router كمثال، يصبح محملات المسارات المكان الطبيعي لتحديد متطلبات البيانات للمسار.
export const Route = createFileRoute('/users/$userId')({
loader: async ({ context, params }) => {
const queryOptions = userQueryOptions(params.userId);
await context.queryClient.ensureQueryData(queryOptions);
},
component: UserProfile,
});
الجزء المهم ليس المحمل نفسه. إنما هو أن الموجه الآن يعرف ما هي البيانات التي يحتاجها المسار مسبقًا، مما يعني أنه يمكنه بدء تحميل تلك البيانات قبل حدوث الانتقال. بمجرد أن تبدأ بالعمل بهذه الطريقة، يصبح سلوك الانتقال مفاجئًا ومفيدًا.
Let the UI Tell You What’s Missing
أحد الأشياء التي أحبها في هذا النهج هو أنه يخلق حلقة تغذية راجعة واضحة جدًا. بمجرد أن يعيش جلب البيانات في محملات المسارات وتبدأ الروابط بتحميل تلك المحملات مسبقًا، يبدأ سلوك واجهة المستخدم بإخبارك بما هو الخطأ.
معظم الناس يرون واجهة المستخدم الفارغة كخطأ ويصلون فورًا إلى مؤشر التحميل. أنا أراها كتعليق.
إذا ظهر جزء من الصفحة بعد التنقل، فهذا يعني أن التطبيق يخبرني أنني نسيت تحميل شيء مسبقًا. لو تم تحميل البيانات مسبقًا بشكل صحيح، لكان انتقال المسار قد انتظرها قبل إتمام التنقل، لذا لا ينبغي أن يبقى شيء ليظهر لاحقًا.
بعبارة أخرى، إذا اكتمل التنقل ولا يزال واجهة المستخدم تملأ نفسها، فهذه إشارة إلى أن جلب البيانات يحدث خارج مسار التحميل المسبق.
الإشارة الأخرى هي عندما يستغرق انتقال المسار وقتًا ملحوظًا.
عندما يبدأ التحميل المسبق عندما تتقاطع الروابط أو يتم التحويم فوقها، يجب أن يمنحنا ذلك نافذة مفيدة بشكل مفاجئ لجلب البيانات قبل حدوث النقر. غالبًا ما تكون هذه الفترة كافية لإكمال التحميل المسبق ولجعل التنقل يبدو فوريًا. ولكن، إذا كان انتقال المسار لا يزال ينتظر، فهذا عادةً يعني إما أن المستخدم نقر قبل أن يحصل التحميل المسبق على الوقت الكافي للانتهاء، أو أن البيانات تستغرق وقتًا طويلاً فعليًا للتحميل.
الحالة الأولى ليست مثيرة للاهتمام بشكل خاص. الحالة الثانية عادةً ما تكون كذلك.
إذا لم تُحمَّل البيانات بعد منحها بضع مئات من المللي ثانية من البداية المسبقة، فمن المفيد البحث عن السبب لتحسين مشكلة الأداء. إذا كان الانتظار لا مفر منه، فهنا يمكننا إظهار مؤشر تحميل عالمي.
حالة تحميل واحدة فقط للتطبيق بأكمله بدلاً من حالات تحميل منفصلة للجدول، الشريط الجانبي، المخطط، وكل ميزة أخرى تجلب البيانات. والأهم من ذلك، أنها تظهر في نفس المكان في كل مرة. يتعلم المستخدمون ما تعنيه، بدلاً من الاضطرار إلى تفسير تجربة تحميل مختلفة لكل ميزة.
GitHub’s loading bar هو مثال جيد على نوع التجربة التي أتحدث عنها. عندما تستغرق عملية التنقل وقتًا أطول قليلًا مما هو متوقع، يظهر مؤشر تحميل صغير في مكان ثابت حتى يكتمل الانتقال. عادةً ما تُظهر مكتبات التوجيه شكلًا ما من حالة الانتقال التي تجعل بناء ذلك أمرًا بسيطًا، مثل useRouterState hook الخاص بـ TanStack Router (https://tanstack.com/router/latest/docs/api/router/useRouterStateHook).
يجب أن نادراً ما يراه المستخدمون لأن التحميل المسبق قد أنجز العمل بالفعل، لكنه موجود كخطة احتياطية للحالات التي لا يمكن للبيانات أن تصل بسرعة كافية.
أنا حتى أُؤخر إظهار المؤشر قليلًا باستخدام شيء مثل spin-delay (https://github.com/smeijer/spin-delay)، لذا إذا اكتمل الانتقال بسرعة نسبية، لا يظهر المؤشر مطلقًا. لا يحتاج المستخدمون إلى رد فعل للانتظارات التي لن يلاحظوها أبدًا.
بسبب ذلك، يمكننا التوقف عن بناء حالات التحميل داخل المكوّنات. تُعيد الاستعلامات البيانات ببساطة. إذا كانت البيانات موجودة، يتم عرضها. إذا لم تكن، لا يُعرض شيء.
const user = useQuery(userQueryOptions(userId));
if (!user.data) return null;
return <UserProfile user={user.data} />;
أنا أفعل ذلك عن قصد.
الهدف ليس إظهار واجهة مستخدم فارغة للمستخدمين. الهدف هو تضخيم الإشارة أثناء التطوير. الهيكل العظمي (skeleton) يخفي المشكلة، لكن إرجاع null يجعلها واضحة.
إذا انتقلت إلى مكان ما وكان جزء من الصفحة فارغًا، أعرف فورًا أن شيئًا ما لم يتم تحميله مسبقًا بشكل صحيح. تلك الفجوات في الواجهة تصبح أداة تشخيصية.
بدلاً من تصميم حالة تحميل أخرى، أحسن استراتيجية التحميل المسبق حتى تختفي المشكلة تمامًا.
تطبيق مبني باستخدام النهج الموصوف. لا حالات تحميل على مستوى المكوّن، ولا هياكل عظمية، ولا تأثيرات وامضة.
ماذا عن تحديث الصفحة؟
التحميل المسبق يعمل فقط عندما يتنقل المستخدمون عبر التطبيق، لكن التحديث يبدأ من الصفر. لا حدث تمرير فوق العنصر، ولا تحميل مسبق، ولا انتقال مسار. لذا، بالنسبة للتحديثات، أتبع نفس النهج.
بدلاً من السماح للمكوّنات الفردية بتحديد شكل التحميل، أتابع ما إذا كان التطبيق لا يزال في مرحلة الاستقرار وأظهر طبقة تغطية كاملة الشاشة حتى يكتمل ذلك.
أقوم بذلك باستخدام تجريد صغير حول مكتبة الاستعلام الخاصة بي التي تُبلغ عن نشاط التحميل إلى موفر. يتتبع الموفر ما إذا كانت الاستعلامات المثبتة لا تزال قيد التحميل، ويعرض طبقة تغطية كاملة الشاشة أثناء استقرار تحميل الصفحة الأولي.
لا يزال التطبيق يُثبت في الخلفية وتستمر الاستعلامات في التنفيذ بشكل طبيعي، لكن المستخدم لا يرى الحالة الجزئية المُعالجة أثناء التحميل. بمجرد إكمال جميع الاستعلامات النشطة، تختفي الطبقة وتصبح التطبيق مرئيًا.
ليس ذلك مثاليًا. أُبطئ إزالة الطبقة قليلاً بحيث لا تتسبب شلالات الطلبات الصغيرة (request waterfalls) فورًا في ظهور المحتوى بعد تحميل الصفحة، لكن لا تزال هناك حالات قد يستمر فيها تدفق البيانات لفترة أطول من نافذة الإبطاء. لا أرى ذلك كمشكلة حالة تحميل، بل كإشارة إلى أن هناك ربما شلالًا يستحق التحقيق. عمليًا، وجدت أن هذا يصبح نادرًا بشكل متزايد بمجرد رفع معظم جلب البيانات إلى حدود المسارات، أي نقلها إلى loaders.
بنفس الطريقة التي تخبرني فيها الأقسام الفارغة أنني نسيت تحميل شيء مسبقًا، غالبًا ما يُظهر المحتوى المتأخر عند التحديث أن تبعيات البيانات يمكن هيكلتها بشكل أفضل.
الهدف النهائي
يمكننا التوقف هنا. أصبح التحميل مسألة تخص التطبيق بدلاً من أن تكون مسألة مكوّن، ويلاحظ المستخدمون حالات تحميل أقل بكثير. لكن إذا كنت صادقًا، فإن طبقة التراكب التي تغطي الشاشة بالكامل لا تزال تبدو كحل وسط.
السلبي هو أن المستخدمين يرون طبقة التراكب في كل تحديث، بينما من المثالي ألا يروا حالات تحميل على الإطلاق.
السبب الرئيسي لوجودها هو أن التحديث يبدأ من الصفر ويجب على التطبيق جلب كل شيء مرة أخرى قبل أن يصبح تفاعليًا. لكن يمكننا حفظ البيانات محليًا، وهذا يغيّر الأمور.
سواء كان ذلك عبر TanStack DB، أو Zero، أو أي نهج آخر تمامًا، الفكرة هي نفسها… الاحتفاظ بكمية كافية من البيانات على الجهاز بحيث يستطيع التطبيق العرض فورًا.
قد يتطلب الزيارة الأولى طلبًا شبكيًا ويظهر التراكب، لكن الزيارة الثانية عادة لا تحتاج ذلك، خاصةً بعد أن يبدأ المستخدم في التنقل داخل التطبيق وتتاح للذاكرة المؤقتة المحلية فرصة للتعبئة.
في تلك المرحلة يصبح التراكب الذي يغطي الشاشة بالكامل خيارًا احتياطيًا بدلاً من أن يكون جزءًا من التجربة العادية، يبقى الهيكل نفسه، وتصبح الانتظارات نادرة بشكل متزايد.
التنقلات فورية لأننا نحمل مسبقًا، وتُشعر الاستمرارية المحلية التحديثات بأنها متشابهة.
الخلاصة
إذا وجدت نفسك تقضي وقتًا مفاجئًا في بناء حالات التحميل، قد يكون من المفيد التراجع خطوة وطرح سؤال مختلف. بدلاً من السؤال عن كيفية تعامل المكوّن مع التحميل، يمكننا السؤال عما إذا كان يجب أن يكون في حالة تحميل أصلاً.
انتقالات المسارات لا تُلغي التحميل، لكنها تُعطينا وسيلة لإعادته إلى ما أعتقد شخصيًا أنه المكان المناسب… على مستوى التطبيق بدلاً من مستوى المكوّن.
عند بدء التحميل المسبق للبيانات بشكلٍ مكثف، يحدث شيءٌ مثير للاهتمام. يختفي الانتظار إلى حد كبير. في تلك المرحلة، لم نعد نحاول تصميم حالات تحميل أفضل، بل نحاول جعلها غير ضرورية.
عندما يظهر شيءٌ متأخرًا، أو يتعطل، أو يرمش عند الظهور، لا تتجه فورًا إلى استخدام الهيكل العظمي. اعتبر ذلك رد فعل. التطبيق يُخبرنا بشيءٍ ما، وغالبًا ما يكون ذلك صحيحًا.
فريق التحرير
مساهم في اختيار المواد وترجمتها وتحريرها للقارئ العربي.