مقابلة مطور Frontend تنتهي كثيراً بسؤال عن الفرق بين let وvar وآخر عن آخر إصدار من الإطار المستخدم، ثم يبدأ المطور عمله فتجد أن زر الشراء لا يستجيب ثانية كاملة بعد تحميل الصفحة، وأن نافذة الدفع لا تصل إليها لوحة المفاتيح، وأن اسم العميل الإنجليزي قلب ترتيب الرقم في الجملة العربية. والسبب أن هذه الأمور لا تظهر في الأسئلة التي تقف عند التعريف، وإنما في الأسئلة التي تسأل لماذا وماذا يحدث عندما.
وفي دليل إدارة المقابلة التقنية شرحنا الطريقة: المستويات، وسلم أسئلة المتابعة، والتقييم بالدرجات، وهذا المقال هو بنك الأسئلة لمطور Frontend، وفيه 47 سؤالاً في كل ما يعمل عليه مطور الواجهة، والأسئلة عن مفاهيم أطر العمل عامة تنطبق على React وVue وAngular، ولكل سؤال إجابة المبتدئ وإجابة الخبير وأسئلة متابعة وعلامات تحذير ورابط للمقال الذي يشرح الموضوع، ثم ثلاثة تمارين عملية شغلناها فعلاً.
وسوف نناقش في هذا المقال ما يلي:
- كيف تستخدم بنك الأسئلة في مقابلة واحدة.
- الأسئلة مرتبة حسب المجال، من أساسيات JavaScript إلى العربية وسهولة الوصول والأداء والأمن.
- ثلاثة تمارين عملية: ترتيب حلقة الأحداث، وزر يحجبه الخيط الرئيسي، ونتائج بحث تصل بالترتيب الخطأ.
كيف تستخدم هذه الأسئلة؟
لا تسأل كل الأسئلة، فهذا بنك تختار منه، والقاعدة أن تأخذ لكل جلسة مدتها ساعة ثمانية أسئلة تقريباً من ثلاثة أو أربعة مجالات مع تمرين عملي واحد، وتترك الوقت لأسئلة المتابعة لأنها ما يكشف المستوى. ولكل سؤال مستوى (مبتدئ أو متوسط أو خبير) يبين أين يكون السؤال في محله، وليس المستوى الوحيد الذي تسأله له.
وفي كل بطاقة: «ماذا يختبر» لتعرف لماذا تسأله، ثم «إجابة المبتدئ» وهي ما نقبله من مبتدئ جيد، و«إجابة الخبير» وهي ما يقوله من عاش المشكلة، ولا تتوقع أن يطابق المرشح النص، وإنما أن يصل إلى نفس الأفكار. ثم «أسئلة المتابعة» لتصعد بها، و«علامات التحذير» التي تخصم بها، ورابط المقال الذي يشرح الموضوع. ولا تخصم من مرشح يعرف الفكرة باسم مختلف في إطار عمل آخر، فـ useEffect في React يقابله watch في Vue، والمهم أن يفهم متى يعمل ولماذا.
والمجالات في هذا المقال:
- أساسيات JavaScript وTypeScript (4 أسئلة)
- حلقة الأحداث Event Loop والخيط الرئيسي (3 أسئلة)
- العرض في المتصفح Rendering (3 أسئلة)
- التخطيط والتنسيق في CSS (3 أسئلة)
- العربية واتجاه الصفحة RTL (3 أسئلة)
- سهولة الوصول Accessibility (3 أسئلة)
- إدارة الحالة State Management (3 أسئلة)
- مفاهيم أطر العمل (3 أسئلة)
- الأداء ومقاييس Core Web Vitals (4 أسئلة)
- الأمن في الواجهة (4 أسئلة)
- التخزين في المتصفح (سؤالان)
- الاختبارات في الواجهة (3 أسئلة)
- أدوات البناء (سؤالان)
- العرض على السيرفر SSR والعرض في المتصفح CSR (سؤالان)
- أنظمة التصميم Design Systems (سؤالان)
- التشخيص بأدوات المطور DevTools (سؤالان)
- مراجعة كود الذكاء الاصطناعي (سؤال واحد)
- تمارين عملية للمقابلة
- الخلاصة
أساسيات JavaScript وTypeScript
1. لماذا تطبع هذه الحلقة الرقم 3 ثلاث مرات مع var؟
مبتدئ ماذا يختبر: النطاق Scope والإغلاق Closure، والفرق بين var وlet. اكتب للمرشح: for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));
إجابة المبتدئ
لأن var ليس لها نطاق الكتلة، فكل الدوال ترى نفس i وقيمتها 3 عند التنفيذ، والحل استخدام let.
إجابة الخبير
الدالة لا تحفظ قيمة i وإنما تحفظ مرجعاً إلى المتغير نفسه، وهذا هو الإغلاق Closure، وvar نطاقها الدالة كلها فهناك متغير واحد، والدوال تعمل بعد انتهاء الحلقة لأن setTimeout يضعها في طابور المهام. ومع let تنشئ اللغة ربطاً جديداً لكل دورة، فكل دالة تغلق على نسختها. ونفس الفكرة تسبب أشهر أخطاء أطر العمل: دالة داخل مكون تغلق على قيمة حالة قديمة Stale Closure، فتقرأ عداداً أو نص بحث من رسم سابق.
أسئلة المتابعة
- كيف كان يحل هذا قبل
let؟ - أين تقابل نفس المشكلة في React داخل
useEffectأو معsetInterval؟ - هل يمكن أن يسبب الإغلاق تسرباً في الذاكرة؟ متى؟
علامات تحذير: يحفظ أن let تحل المشكلة دون أن يستطيع شرح السبب.
مقالات تشرح الموضوع: تمارين Kata في البرمجة بثلاثة مستويات مع الحلول، والذاكرة للمبرمجين: كيف تعمل ال Stack وال Heap.
2. لماذا يصبح this غير معرف عندما تمرر دالة الكلاس كمعالج حدث؟
متوسط ماذا يختبر: ربط this في JavaScript وأنه يحدد عند الاستدعاء وليس عند التعريف.
إجابة المبتدئ
لأن this يتغير عند تمرير الدالة، والحل استخدام دالة سهمية Arrow Function أو bind.
إجابة الخبير
قيمة this في الدالة العادية تحددها طريقة الاستدعاء: obj.method() تعطي obj، أما const f = obj.method; f() فتعطي undefined في الوضع الصارم Strict Mode، وجسم الكلاس دائماً صارم. وعندما تمرر this.save إلى addEventListener فأنت تمرر الدالة وحدها. والدالة السهمية لا تملك this خاصاً بها وإنما تأخذه من المكان الذي عرفت فيه، لذلك تحل المشكلة كحقل في الكلاس، مع ملاحظة أن كل نسخة تحصل على دالة جديدة، وأن removeEventListener يحتاج نفس المرجع.
أسئلة المتابعة
- لماذا لا يعمل
removeEventListener('click', this.save.bind(this))؟ - ما قيمة
thisداخل معالج حدث مسجل بدالة عادية؟
علامات تحذير: يستخدم bind في كل مكان دون أن يعرف لماذا.
المقال الذي يشرح الموضوع: أنماط البرمجة وأنظمة الأنواع: لماذا تختلف اللغات؟.
3. لماذا لم يصل الخطأ إلى catch في هذا الكود غير المتزامن؟
متوسط ماذا يختبر: التعامل مع أخطاء الوعود Promises، وawait المنسي، وforEach مع async. مثال: try { items.forEach(async (x) => await save(x)); } catch (e) { ... }
إجابة المبتدئ
لأن forEach لا ينتظر الدوال غير المتزامنة، فالأفضل استخدام حلقة for...of مع await.
إجابة الخبير
forEach يستدعي الدالة ويتجاهل الوعد الذي ترجعه، فتخرج الحلقة فوراً وينتهي ال try قبل أن يفشل أي حفظ، وتصبح الأخطاء وعوداً مرفوضة لا يعالجها أحد Unhandled Rejection. والحل يعتمد على ما أريده: for...of مع await للتنفيذ بالترتيب، أو Promise.all للتوازي مع فشل سريع عند أول خطأ، أو Promise.allSettled لأعرف نتيجة كل عنصر. ومع التوازي أفكر في الحد: ألف طلب معاً قد يسقط السيرفر أو يصطدم بحد المتصفح للاتصالات.أسئلة المتابعة
- ما الفرق بين
Promise.allوPromise.allSettledوPromise.any؟ - كيف تنفذ ألف طلب بحد أقصى خمسة في نفس الوقت؟
- ماذا يحدث لوعد مرفوض دون معالجة في Node.js الحديث؟
علامات تحذير: يضع async على كل دالة دون أن يعرف ماذا ترجع.
مقالات تشرح الموضوع: كيف تتعامل لغات البرمجة مع الأخطاء؟، وThreads وasync/await: كود لا يتجمد ولا يضيع بياناته.
4. ما الفرق بين any وunknown في TypeScript، ولماذا لا تحميك الأنواع من رد ال API؟
متوسط ماذا يختبر: فهم أن الأنواع تختفي بعد الترجمة، والتحقق من البيانات عند الحدود.
إجابة المبتدئ
any يلغي فحص الأنواع، وunknown يجبرك على التحقق قبل الاستخدام، لذلك unknown أكثر أماناً.إجابة الخبير
TypeScript يفحص الكود وقت الترجمة فقط، ثم تحذف الأنواع ويبقى JavaScript، فعندما تكتب const user = await res.json() as User فأنت تعد المترجم بشيء لا يتحقق منه أحد، وإذا غير ال Backend اسم حقل فسوف يمر الكود ويفشل لاحقاً بعيداً عن السبب. لذلك أعامل كل ما يأتي من الخارج (ال API، وlocalStorage، ورسائل postMessage) على أنه unknown، وأتحقق منه عند الحدود بمكتبة مثل Zod أو بدوال حماية Type Guards، وأستنتج النوع من ال Schema نفسه حتى لا يكون هناك تعريفان يختلفان.
أسئلة المتابعة
- ما هو Type Guard ومتى تكتبه بنفسك؟
- كيف تولد الأنواع من OpenAPI، وما الذي لا يحميك منه ذلك؟
علامات تحذير: يستخدم as وany لإسكات المترجم كعادة.
مقالات تشرح الموضوع: أنماط البرمجة وأنظمة الأنواع: لماذا تختلف اللغات؟، وكيف تصمم API لا تضطر إلى كسره بعد سنة.
حلقة الأحداث Event Loop والخيط الرئيسي
5. ما الفرق بين المهمة Task والمهمة الصغرى Microtask في المتصفح؟
متوسط ماذا يختبر: ترتيب التنفيذ في حلقة الأحداث، وهو التمرين العملي الأول في هذا المقال.
إجابة المبتدئ
setTimeout تذهب إلى طابور المهام، وPromise.then تذهب إلى طابور المهام الصغرى الذي ينفذ أولاً.إجابة الخبير
بعد كل مهمة (سكربت، أو حدث، أو مؤقت) يفرغ المتصفح طابور المهام الصغرى كله، بما فيه الصغرى التي تضيفها الصغرى نفسها، قبل أن ينتقل إلى المهمة التالية أو إلى رسم الإطار. لذلك سلسلة then لا نهائية تجمد الصفحة تماماً مثل حلقة while، لأن المتصفح لا يصل أبداً إلى الرسم. وawait هو then على ما بعده، فالسطر بعد await مهمة صغرى. ومؤقت بصفر لا يعني الآن، وإنما بعد انتهاء المهمة الحالية وكل الصغرى، وقد يتأخر أكثر إذا كان الخيط مشغولاً أو كانت الصفحة في الخلفية.
أسئلة المتابعة
- أين يقع
requestAnimationFrameمن هذا الترتيب؟ - كيف يختلف
process.nextTickفي Node.js؟ - لماذا قد يتأخر
setTimeout(fn, 0)ثانية كاملة في تبويب في الخلفية؟
علامات تحذير: يعتقد أن الوعود تعمل في خيط آخر بالتوازي.
المقال الذي يشرح الموضوع: Threads وasync/await: كود لا يتجمد ولا يضيع بياناته.
6. زر الشراء لا يستجيب لثانية بعد تحميل الصفحة، ما السبب المحتمل؟
خبير ماذا يختبر: المهام الطويلة Long Tasks وأثرها على INP، وهو التمرين العملي الثاني.
إجابة المبتدئ
ربما هناك كود ثقيل يعمل عند التحميل، فنؤجله أو نضعه في setTimeout.
إجابة الخبير
المتصفح ينفذ معالج النقرة في نفس الخيط الذي ينفذ JavaScript الصفحة، فإذا كانت هناك مهمة واحدة تأخذ ثانية (تحليل JSON كبير، أو بناء جدول من آلاف الصفوف، أو Hydration لصفحة كبيرة) فالنقرة تنتظر في الطابور حتى تنتهي، وهذا يظهر في مقياس INP كتأخير إدخال Input Delay. وأي مهمة أطول من 50 ملي ثانية تعتبر مهمة طويلة، وأجدها في تبويب Performance في DevTools. والحل تقسيم العمل إلى قطع مع إعطاء المتصفح فرصة بينها (scheduler.yield() أو setTimeout)، أو نقله إلى Web Worker إذا لم يحتج إلى ال DOM، أو تقليله أصلاً.
أسئلة المتابعة
- ما الفرق بين
setTimeoutوscheduler.yield()كطريقة لإعطاء المتصفح فرصة؟ - ما الذي لا تستطيع فعله داخل Web Worker؟
- كيف تقيس INP للمستخدمين الحقيقيين وليس على جهازك فقط؟
علامات تحذير: يقترح async على الدالة الثقيلة كأنه ينقلها إلى خيط آخر.
مقالات تشرح الموضوع: Threads وasync/await: كود لا يتجمد ولا يضيع بياناته، وتتبع الطلب البطيء من المتصفح إلى قاعدة البيانات.
7. متى تستخدم requestAnimationFrame بدلاً من setTimeout للحركة؟
متوسط ماذا يختبر: علاقة الكود بإطارات العرض Frames.
إجابة المبتدئ
requestAnimationFrame أنعم للحركة لأنه يعمل مع تحديث الشاشة.إجابة الخبير
المتصفح يرسم بمعدل الشاشة (60 إطاراً في الثانية تعني 16.7 ملي ثانية للإطار، وأسرع في الشاشات الحديثة)، وrequestAnimationFrame يستدعي دالتك قبل الرسم التالي مباشرة، فيكون التحديث متزامناً مع الإطار، ويتوقف تلقائياً في التبويب المخفي فيوفر البطارية. أما setTimeout(16) فلا يعرف متى يرسم المتصفح، فيأتي أحياناً مرتين في إطار وأحياناً لا يأتي. وفي أغلب الحالات الأفضل ألا تحرك بالكود أصلاً، وإنما بـ CSS Transitions على transform وopacity التي ينفذها ال Compositor دون الخيط الرئيسي.
أسئلة المتابعة
- لماذا تبقى حركة
transformناعمة والخيط الرئيسي مشغول؟ - كيف تقرأ وتكتب ال DOM في نفس الإطار دون Layout Thrashing؟
علامات تحذير: يحرك العناصر بتغيير top وleft بمؤقت.
المقال الذي يشرح الموضوع: كيف تعمل الشاشة من الداخل: من بطاقة الرسومات إلى عينك.
العرض في المتصفح Rendering
8. ماذا يحدث من لحظة وصول ملف HTML حتى تظهر الصفحة؟
مبتدئ ماذا يختبر: مسار العرض الحرج Critical Rendering Path بمستوى يكفي لفهم الأداء.
إجابة المبتدئ
المتصفح يقرأ HTML ويبني ال DOM، ثم يقرأ CSS، ثم يرسم الصفحة، وملفات JavaScript قد تؤخر ذلك.
إجابة الخبير
المتصفح يحلل HTML إلى DOM وCSS إلى CSSOM، ويجمعهما في شجرة العرض، ثم يحسب المواقع والأحجام Layout، ثم يرسم Paint، ثم يركب الطبقات Composite. وCSS يحجب الرسم لأن المتصفح لا يرسم صفحة دون أنماطها، وسكربت عادي في head يحجب التحليل حتى يحمل وينفذ، لذلك نستخدم defer أو type="module". والخط الذي لم يحمل بعد قد يخفي النص (FOIT)، والحل font-display: swap مع تحميل مسبق للخط العربي لأن حجمه أكبر غالباً. وأهم ما يؤثر على LCP: سرعة وصول HTML، وأولوية تحميل الصورة الكبيرة أو الخط.
أسئلة المتابعة
- ما الفرق بين
deferوasyncعلى السكربت؟ - لماذا قد يكون الخط العربي سبب بطء LCP؟
- ماذا يفعل
fetchpriority="high"؟
علامات تحذير: لا يعرف أن CSS يحجب الرسم، أو يضع السكربتات في head دون defer.
مقالات تشرح الموضوع: HTTP وHTTPS ببساطة: ماذا يحدث بين الرابط والصفحة، وكيف تعمل الشاشة من الداخل: من بطاقة الرسومات إلى عينك.
9. ما هو Layout Thrashing، وكيف تكتشفه؟
متوسط ماذا يختبر: إعادة حساب التخطيط Reflow الإجباري بسبب ترتيب القراءة والكتابة.
إجابة المبتدئ
عندما نغير التصميم كثيراً يعيد المتصفح الحساب، والحل تقليل التغييرات.
إجابة الخبير
المتصفح يؤجل حساب التخطيط إلى ما قبل الرسم، ولكن إذا قرأت قيمة تعتمد عليه مثل offsetHeight أو getBoundingClientRect() بعد أن كتبت تغييراً، يضطر لحسابه فوراً. وفي حلقة تقرأ وتكتب لكل عنصر يحدث الحساب ألف مرة بدلاً من مرة، وهذا Layout Thrashing. والحل أن أقرأ كل القيم أولاً ثم أكتب كل التغييرات، أو أؤجل الكتابة إلى requestAnimationFrame. وأكتشفه في تبويب Performance حيث يظهر Layout بلون بنفسجي متكرر مع تحذير Forced reflow.
أسئلة المتابعة
- ما الخصائص التي تغيير قيمتها لا يحتاج Layout ولا Paint؟
- كيف يساعدك
content-visibility: autoفي صفحة طويلة؟
علامات تحذير: لم يفتح تبويب Performance أبداً.
المقال الذي يشرح الموضوع: كيف تعمل الشاشة من الداخل: من بطاقة الرسومات إلى عينك.
10. لماذا تقفز الصفحة أثناء التحميل فيضغط المستخدم على الزر الخطأ؟
متوسط ماذا يختبر: إزاحة التخطيط CLS وأسبابها الشائعة.
إجابة المبتدئ
لأن الصور والإعلانات تظهر بعد النص فتدفع المحتوى إلى الأسفل، والحل وضع أبعاد للصور.
إجابة الخبير
هذه إزاحة التخطيط التي يقيسها CLS، والهدف 0.1 أو أقل. وأسبابها: صور وفيديو دون width وheight أو aspect-ratio، ومحتوى يحقن فوق المحتوى الموجود (لافتة الكوكيز وإعلان)، وخط يحمل متأخراً بمقاسات مختلفة عن البديل، ومكونات تحمل بياناتها ثم تتمدد. والحلول: حجز المساحة مسبقاً، ووضع المحتوى المتأخر في مكان محجوز أو فوق المحتوى بـ position، وضبط مقاسات الخط البديل بـ size-adjust. والإزاحة خلال 500 ملي ثانية بعد تفاعل المستخدم لا تحسب لأنها متوقعة.
أسئلة المتابعة
- كيف تعرف أي عنصر سبب الإزاحة؟
- لماذا قد يكون CLS سيئاً عند المستخدمين وجيداً في Lighthouse؟
علامات تحذير: لا يعرف أن أبعاد الصورة في HTML تحجز المساحة.
المقال الذي يشرح الموضوع: صيغ الصور: كيف تختار الصيغة المناسبة لموقعك.
التخطيط والتنسيق في CSS
11. لماذا لا تطبق قاعدة CSS كتبتها رغم أنها صحيحة؟
مبتدئ ماذا يختبر: التتالي Cascade والخصوصية Specificity وترتيب الملفات.
إجابة المبتدئ
لأن قاعدة أخرى لها أولوية أعلى، مثل ID أو !important، والحل أن أجعل المحدد أكثر تحديداً.
إجابة الخبير
أفتح Elements في DevTools وأرى القاعدة مشطوبة ومن الذي غلبها. والترتيب: الأصل (أنماط المتصفح ثم المؤلف) والطبقة ثم الخصوصية ثم ترتيب الظهور، والخصوصية تحسب كثلاثة أرقام (ID، ثم Class وخاصية وPseudo-class، ثم العنصر). ورفع الخصوصية لتغلب قاعدة أخرى يبدأ حرباً لا تنتهي، والحل الأفضل تنظيم الأنماط: @layer لترتيب الطبقات (Reset ثم مكتبة ثم مكونات ثم استثناءات) بحيث تغلب الطبقة المتأخرة مهما كانت خصوصية الأولى، ومحددات بخصوصية منخفضة ومتساوية، و:where() عندما أريد خصوصية صفر.
أسئلة المتابعة
- ما خصوصية
:is(#a, .b)، وما خصوصية:where(#a)؟ - كيف تمنع مكتبة خارجية من كسر أنماطك؟
علامات تحذير: يحل كل تعارض بـ !important.
المقال الذي يشرح الموضوع: مبادئ التصميم Design Principles بأمثلة قبل وبعد.
12. متى تختار Grid ومتى تختار Flexbox؟
مبتدئ ماذا يختبر: التخطيط في بعد واحد أم بعدين.
إجابة المبتدئ
Flexbox لصف أو عمود واحد مثل شريط التنقل، وGrid للتخطيطات بالصفوف والأعمدة مثل الصفحة كلها.
إجابة الخبير
Flexbox يوزع العناصر على محور واحد ويترك المحتوى يحدد الأحجام، فهو مناسب للمكونات (أزرار، وشريط أدوات، وبطاقة)، وGrid يعرف الصفوف والأعمدة من الحاوي فتصطف العناصر في البعدين، فهو مناسب للتخطيط وللبطاقات التي يجب أن تتساوى. وgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr)) يعطي شبكة متجاوبة دون أي Media Query. والاثنان يحترمان اتجاه الصفحة تلقائياً، فـ justify-content: flex-start تبدأ من اليمين في صفحة RTL، وهذا سبب آخر لتجنب float وleft.
أسئلة المتابعة
- ما فائدة
subgrid؟ - ما هي Container Queries، ولماذا هي أفضل من Media Queries للمكونات؟
علامات تحذير: يستخدم float أو مواقع مطلقة للتخطيط.
المقال الذي يشرح الموضوع: مبادئ التصميم Design Principles بأمثلة قبل وبعد.
13. وضعت z-index بقيمة 9999 والنافذة ما زالت خلف العنصر، لماذا؟
متوسط ماذا يختبر: سياق التكديس Stacking Context.
إجابة المبتدئ
ربما العنصر ليس له position، فـ z-index لا يعمل دونها.
إجابة الخبير
هذا سبب، والسبب الأشهر أن النافذة داخل أب أنشأ سياق تكديس Stacking Context جديداً (بـ transform أو opacity أقل من 1 أو filter أو position: fixed أو z-index على عنصر موضوع)، فقيمة 9999 تقارن فقط مع إخوتها داخل هذا السياق، والأب نفسه خلف العنصر الآخر. والحل ليس رقماً أكبر، وإنما نقل النافذة إلى نهاية body عبر Portal، أو استخدام عنصر <dialog> مع showModal() الذي يضع النافذة في الطبقة العليا Top Layer فوق كل شيء، ويعطي معها إدارة التركيز وزر Escape.
أسئلة المتابعة
- ما هي الطبقة العليا Top Layer، وما العناصر التي تدخلها؟
- كيف تعرف في DevTools أن عنصراً أنشأ سياق تكديس؟
علامات تحذير: يرفع الرقم إلى 99999.
المقال الذي يشرح الموضوع: اختبار سهولة الوصول Accessibility لموقع عربي.
العربية واتجاه الصفحة RTL
14. كيف تكتب CSS يعمل بالعربية والإنجليزية دون ملفين؟
مبتدئ ماذا يختبر: الخصائص المنطقية Logical Properties والسمة dir.
إجابة المبتدئ
نضع dir="rtl" على الصفحة العربية، ونقلب margin-left إلى margin-right في ملف خاص.
إجابة الخبير
أضع dir وlang على html، وأكتب بالخصائص المنطقية: margin-inline-start بدلاً من margin-left، وpadding-inline، وinset-inline-end، وtext-align: start، وborder-start-start-radius، فتنقلب كلها تلقائياً مع الاتجاه. ويبقى ما يجب قلبه يدوياً: الأيقونات التي لها اتجاه (سهم التالي والرجوع) بـ :dir(rtl) أو transform: scaleX(-1)، أما الأيقونات التي لا اتجاه لها (البحث، والإعدادات، وعلامة التشغيل في بعض الأنظمة) فلا تقلب. وtransform وtranslateX لا تنقلب تلقائياً، وهذا يكسر القوائم المنزلقة.
أسئلة المتابعة
- أي أيقونة تقلبها وأيها لا تقلبها؟ أعط أمثلة.
- كيف تختبر الواجهة باتجاهين في CI؟
علامات تحذير: يكتب ملف CSS منفصلاً للعربية ويعدل كل left يدوياً.
المقال الذي يشرح الموضوع: اختبار سهولة الوصول Accessibility لموقع عربي.
15. اسم مستخدم إنجليزي داخل جملة عربية قلب ترتيب الرقم بعده، لماذا؟
متوسط ماذا يختبر: خوارزمية الاتجاه الثنائي Bidi وعزل النصوص.
إجابة المبتدئ
لأن المتصفح يخلط الاتجاهين، والحل وضع الاسم في span مع dir="ltr".
إجابة الخبير
خوارزمية Unicode Bidi تحدد اتجاه كل حرف، والأرقام وعلامات الترقيم محايدة تأخذ اتجاه ما حولها، فرقم بعد كلمة إنجليزية في جملة عربية قد يلتصق بالكلمة الإنجليزية ويظهر في المكان الخطأ. والحل لكل نص يأتي من المستخدم أو من البيانات هو العزل: عنصر <bdi> أو dir="auto" الذي يحدد الاتجاه من أول حرف قوي، أو في النصوص الخالصة علامات العزل في Unicode مثل FSI وPDI. ونفس المشكلة في حقول الإدخال: حقل البريد والرابط ورقم الهاتف يكون dir="ltr" حتى في الصفحة العربية.
أسئلة المتابعة
- لماذا يظهر الرقم السالب أحياناً بالعلامة في الجهة الخطأ؟
- كيف تعرض اسم ملف عربي فيه امتداد إنجليزي في قائمة؟
علامات تحذير: لم يسمع بعنصر bdi ويحل كل حالة بمسافات أو رموز.
مقالات تشرح الموضوع: اختبار سهولة الوصول Accessibility لموقع عربي، والترميز Encoding للمطورين: Base64 وأخواته.
16. المستخدم يكتب «احمد» والاسم محفوظ «أحمد» فلا تظهر النتيجة، ماذا تفعل؟
خبير ماذا يختبر: التعامل مع النص العربي: التطبيع Normalization والتشكيل وحد الأحرف.
إجابة المبتدئ
نحول الهمزات إلى ألف قبل المقارنة في البحث.
إجابة الخبير
البحث العربي يحتاج تطبيعاً على الجهتين، في النص المحفوظ وفي الاستعلام: توحيد أشكال الألف (أ إ آ إلى ا)، والتاء المربوطة والهاء في نهاية الكلمة، والياء والألف المقصورة، وحذف التشكيل والتطويل (ـ)، مع حفظ النص الأصلي للعرض. ولا أفعل ذلك في الواجهة وحدها، لأن البحث الحقيقي في ال Backend أو محرك البحث. وفي الواجهة نفسها أنتبه لعد الأحرف: "مرحباً".length يعد التنوين حرفاً، والرمز التعبيري قد يأخذ وحدتين، فحد «280 حرفاً» يحتاج Intl.Segmenter ليعد ما يراه المستخدم.
أسئلة المتابعة
- لماذا يقطع
slice(0, 20)رمزاً تعبيرياً إلى نصفين؟ - كيف ترتب أسماء عربية أبجدياً بشكل صحيح؟ (
Intl.Collator)
علامات تحذير: يرى المشكلة في قاعدة البيانات فقط، أو لا يعرف أن التنوين حرف منفصل.
مقالات تشرح الموضوع: البحث في النصوص العربية مع OpenSearch وMeilisearch، وتمارين Kata في البرمجة بثلاثة مستويات مع الحلول، والتعبيرات النمطية Regex للمبتدئين، مع النص العربي.
سهولة الوصول Accessibility
17. ما المشكلة في زر مصنوع من div مع onclick؟
مبتدئ ماذا يختبر: HTML الدلالي Semantic HTML وما يعطيه مجاناً.
إجابة المبتدئ
قارئ الشاشة لا يعرف أنه زر، فيجب أن نضيف role="button".
إجابة الخبير
عنصر <button> يعطي مجاناً: الوصول بزر Tab، والتفعيل بـ Enter والمسافة، والدور الصحيح لقارئ الشاشة، وحالة disabled، والإرسال داخل النماذج. وdiv مع role="button" يعطي الدور فقط، وعليك أن تضيف tabindex="0" ومعالج لوحة المفاتيح بنفسك، وسوف تنسى شيئاً. والقاعدة الأولى في ARIA أن العنصر الأصلي أفضل من أي ARIA. وأختبر بنفسي: أترك الفأرة وأتنقل في الصفحة بلوحة المفاتيح، ثم أشغل قارئ شاشة مثل NVDA أو VoiceOver.
أسئلة المتابعة
- متى تستخدم رابطاً
<a>ومتى تستخدم زراً؟ - ماذا يفوت الفحص الآلي مثل axe في صفحة عربية؟
علامات تحذير: يعتبر سهولة الوصول ميزة إضافية لاحقة.
المقال الذي يشرح الموضوع: اختبار سهولة الوصول Accessibility لموقع عربي.
18. كيف يعرف قارئ الشاشة اسم أيقونة الحذف في كل صف من الجدول؟
متوسط ماذا يختبر: الاسم المتاح Accessible Name والتسميات.
إجابة المبتدئ
نضيف aria-label="حذف" على الزر.
إجابة الخبير
الاسم «حذف» وحده لا يكفي في جدول فيه خمسون زر حذف، فمستخدم قارئ الشاشة يتنقل بين الأزرار ويسمع «حذف، حذف، حذف»، لذلك الاسم يجب أن يحدد الصف: «حذف فاتورة 1042»، إما بـ aria-label كاملة أو بـ aria-labelledby يشير إلى خلية الرقم. وللحقول <label for> مرتبطة، وليس placeholder لأنه يختفي عند الكتابة، والأخطاء مربوطة بالحقل بـ aria-describedby. وتسمية الأيقونة باللغة الصحيحة تحتاج lang صحيحاً على الصفحة حتى يقرأها القارئ بالصوت العربي.
أسئلة المتابعة
- ما الفرق بين
aria-labelوaria-labelledbyوaria-describedby؟ - كيف تعلن رسالة «تم الحفظ» دون أن ينتقل التركيز؟ (
aria-live)
علامات تحذير: يعتمد على placeholder كتسمية للحقول.
المقال الذي يشرح الموضوع: اختبار سهولة الوصول Accessibility لموقع عربي.
19. فتحت نافذة منبثقة، أين يذهب التركيز وأين يعود عند الإغلاق؟
خبير ماذا يختبر: إدارة التركيز Focus Management في المكونات التفاعلية.
إجابة المبتدئ
نضع التركيز على أول حقل في النافذة.
إجابة الخبير
عند الفتح ينتقل التركيز إلى النافذة (أول عنصر تفاعلي أو عنوانها)، ويبقى محصوراً داخلها فلا يخرج Tab إلى الصفحة خلفها، والصفحة خلفها تصبح غير متاحة بـ inert، وEscape يغلقها. وعند الإغلاق يعود التركيز إلى الزر الذي فتحها، وإلا وجد مستخدم لوحة المفاتيح نفسه في أعلى الصفحة. وعنصر <dialog> مع showModal() يفعل أغلب هذا تلقائياً اليوم. وفي تطبيقات الصفحة الواحدة SPA نفس المشكلة عند تغيير الصفحة: التركيز يجب أن ينتقل إلى عنوان الصفحة الجديدة ويعلن تغييرها.
أسئلة المتابعة
- ما هو
inert؟ - كيف تتعامل مع التركيز بعد حذف العنصر الذي عليه التركيز؟
علامات تحذير: لا يعرف أن التركيز يجب أن يعود.
المقال الذي يشرح الموضوع: اختبار سهولة الوصول Accessibility لموقع عربي.
إدارة الحالة State Management
20. هل تضع بيانات ال API في المخزن العام Global Store؟
متوسط ماذا يختبر: الفرق بين حالة السيرفر Server State وحالة الواجهة.
إجابة المبتدئ
نعم، نجلب البيانات ونضعها في Redux أو Zustand حتى تصل إليها كل المكونات.
إجابة الخبير
بيانات ال API ليست حالتنا، وإنما نسخة من حالة السيرفر قد تكون قديمة، وتحتاج: كاشاً بمفتاح، وإعادة الجلب عند العودة للتبويب، وإلغاء الطلبات القديمة، وحالات التحميل والخطأ، ومنع الطلب المكرر من عدة مكونات، والإبطال بعد التعديل. وكتابة هذا يدوياً في مخزن عام تعيد اختراع مكتبة مثل TanStack Query أو SWR. لذلك أفصل: حالة السيرفر في مكتبة كاش، وحالة الواجهة (القائمة مفتوحة، والتبويب المختار) محلية في المكون أو في الرابط URL، والمخزن العام يبقى صغيراً لما هو عام فعلاً مثل المستخدم الحالي.
أسئلة المتابعة
- لماذا يكون الرابط مكاناً جيداً لحالة الفلاتر والصفحة؟
- كيف تحدث الواجهة فوراً قبل رد السيرفر، وماذا تفعل إذا فشل؟ (Optimistic Update)
علامات تحذير: يضع كل شيء في مخزن عام واحد كقاعدة.
مقالات تشرح الموضوع: التخزين المؤقت Caching بالتفصيل: الاستراتيجيات والإبطال، وكيف تصمم API لا تضطر إلى كسره بعد سنة.
21. لماذا لا تحفظ في الحالة قيمة يمكن حسابها من حالة أخرى؟
مبتدئ ماذا يختبر: الحالة المشتقة Derived State ومصدر الحقيقة الواحد.
إجابة المبتدئ
لأنها تكرار، والأفضل حسابها عند العرض.
إجابة الخبير
عندما تحفظ items وtotal معاً فقد صنعت مصدرين للحقيقة، وكل تعديل على items يجب أن يتذكر تحديث total، ويوماً ما ينساه أحد. والحساب عند العرض صحيح دائماً، وإذا كان مكلفاً فعلاً أحفظ نتيجته Memoization مرتبطة بمدخلاته وليس في الحالة. ونفس القاعدة مع نسخ ال Props إلى حالة محلية في المكون: نسخة لا تتحدث عندما يتغير ال Prop، وهو خطأ شائع جداً.
أسئلة المتابعة
- متى يكون نسخ ال Prop إلى حالة صحيحاً؟ (قيمة ابتدائية لنموذج)
- كيف تقرر أن الحساب مكلف بما يكفي لحفظه؟
علامات تحذير: يستخدم useEffect لمزامنة حالتين.
المقال الذي يشرح الموضوع: مبادئ التصميم Design Principles بأمثلة قبل وبعد.
22. نموذج طويل فيه عشرون حقلاً، كل حرف يعيد رسم النموذج كله، ماذا تفعل؟
متوسط ماذا يختبر: الحقول المتحكم بها Controlled والحرة Uncontrolled، وأداء النماذج.
إجابة المبتدئ
نقسم النموذج إلى مكونات أصغر.
إجابة الخبير
الحقل المتحكم به يحفظ قيمته في حالة المكون، فكل حرف يحدث الحالة ويعيد رسم المكون الذي يملكها، وإذا كانت الحالة في أعلى النموذج فكل الحقول ترسم مع كل حرف. والحلول: حقول حرة يقرؤها النموذج عند الإرسال (FormData، وهذا ما تفعله مكتبات مثل React Hook Form)، أو نقل الحالة إلى أقرب مكون يحتاجها، أو التحقق عند مغادرة الحقل بدلاً من كل حرف. والتحقق في الواجهة للراحة فقط، فال Backend يتحقق من كل شيء مرة أخرى.
أسئلة المتابعة
- متى تحتاج الحقل المتحكم به فعلاً؟
- كيف تمنع إرسال النموذج مرتين عند الضغط المزدوج؟
علامات تحذير: يعتمد على تحقق الواجهة وحده.
المقال الذي يشرح الموضوع: البرمجة الآمنة Secure Coding بأمثلة من أربع لغات.
مفاهيم أطر العمل
23. لماذا تحتاج القوائم مفتاحاً key، ولماذا لا تستخدم رقم العنصر؟
مبتدئ ماذا يختبر: كيف يطابق إطار العمل العناصر بين رسمين (React وVue وغيرهما).
إجابة المبتدئ
المفتاح يساعد الإطار على معرفة العناصر، ورقم العنصر يسبب مشكلات عند تغيير الترتيب.
إجابة الخبير
الإطار يقارن الرسم الجديد بالقديم ويطابق العناصر بالمفتاح، فإذا كان المفتاح رقم الموقع وحذفت العنصر الأول، فالعنصر الذي كان ثانياً يأخذ مفتاح الأول ويرث حالته الداخلية: نص مكتوب في حقل، أو مربع محدد، أو تركيز. وهذا خطأ صامت يظهر للمستخدم كبيانات في الصف الخطأ. والمفتاح الصحيح معرف ثابت من البيانات. والمفتاح أيضاً أداة: تغييره عمداً يجبر الإطار على إنشاء المكون من جديد وتصفير حالته، مثل نموذج تعديل عند الانتقال من مستخدم لآخر.
أسئلة المتابعة
- كيف تستخدم المفتاح لتصفير حالة مكون؟
- ماذا لو لم يكن للبيانات معرف؟
علامات تحذير: يستخدم Math.random() كمفتاح.
المقال الذي يشرح الموضوع: هياكل البيانات Data Structures للمبتدئين.
24. متى لا تحتاج useEffect، ومتى يسبب حلقة لا تنتهي؟
متوسط ماذا يختبر: التأثيرات الجانبية Side Effects ومصفوفة الاعتماديات (والفكرة نفسها في watch في Vue).
إجابة المبتدئ
نستخدم useEffect لجلب البيانات، والحلقة تحدث إذا غيرنا الحالة داخلها دون اعتماديات صحيحة.
إجابة الخبير
التأثير لمزامنة المكون مع نظام خارجي (اشتراك، أو مؤقت، أو مكتبة خرائط)، أما حساب قيمة من الحالة أو الاستجابة لنقرة فمكانه أثناء الرسم أو في معالج الحدث. والحلقة تحدث عندما يغير التأثير قيمة هو يعتمد عليها، أو عندما يعتمد على كائن أو دالة تنشأ من جديد مع كل رسم فتختلف مرجعياً كل مرة. وكل تأثير يشترك في شيء يحتاج دالة تنظيف، وإلا تراكمت الاشتراكات، وجلب البيانات في التأثير يحتاج إلغاء الطلب القديم، وإلا وصلت النتيجة القديمة بعد الجديدة (التمرين العملي الثالث).
أسئلة المتابعة
- لماذا يعمل التأثير مرتين في وضع التطوير Strict Mode؟
- ما هي Stale Closure في مؤقت داخل التأثير؟
علامات تحذير: يعطل تحذير الاعتماديات في ال Linter بدلاً من فهمه.
المقال الذي يشرح الموضوع: Threads وasync/await: كود لا يتجمد ولا يضيع بياناته.
25. الصفحة بطيئة لأن مكوناً يعاد رسمه كثيراً، كيف تجده وتصلحه؟
خبير ماذا يختبر: قياس أداء الإطار قبل التحسين، والتحسين في المكان الصحيح.
إجابة المبتدئ
نضع memo وuseMemo على المكونات حتى لا تعيد الرسم.
إجابة الخبير
أقيس أولاً بأداة Profiler الخاصة بالإطار لأرى أي مكون يرسم ولماذا وكم يأخذ، لأن إعادة الرسم الرخيصة ليست مشكلة. والأسباب الشائعة: حالة موضوعة أعلى من اللازم فتعيد رسم شجرة كبيرة، وسياق Context يتغير قيمته مع كل رسم فيعيد رسم كل مستهلكيه، وقائمة طويلة ترسم كل عناصرها. والحلول بالترتيب: نقل الحالة إلى الأسفل، وتقسيم السياق، والقوائم الافتراضية Virtualization للقوائم الطويلة، ثم memo في المكان الذي أثبت القياس أنه يفيد، مع ملاحظة أن memo لا يفيد إذا كانت ال Props كائنات جديدة في كل رسم.
أسئلة المتابعة
- متى يكون
useMemoأبطأ من عدم استخدامه؟ - ما هي القوائم الافتراضية وما مشكلاتها مع البحث داخل الصفحة وسهولة الوصول؟
علامات تحذير: يضع memo على كل شيء دون قياس.
مقالات تشرح الموضوع: تتبع الطلب البطيء من المتصفح إلى قاعدة البيانات، وBig-O للمبتدئين: لماذا يبطؤ كودك عندما تكبر البيانات.
الأداء ومقاييس Core Web Vitals
26. ماذا تقيس مقاييس LCP وINP وCLS، وما الحد الجيد لكل منها؟
مبتدئ ماذا يختبر: معرفة المقاييس التي يقيس بها Google تجربة المستخدم.
إجابة المبتدئ
LCP سرعة ظهور المحتوى الرئيسي، وINP سرعة الاستجابة للتفاعل، وCLS ثبات الصفحة.
إجابة الخبير
بحسب موقع web.dev: LCP زمن ظهور أكبر عنصر في الشاشة الأولى والجيد 2.5 ثانية أو أقل، وINP أطول زمن من التفاعل إلى الرسم التالي خلال زيارة الصفحة والجيد 200 ملي ثانية أو أقل، وCLS مجموع الإزاحات غير المتوقعة والجيد 0.1 أو أقل، والقياس عند النسبة المئوية 75 من زيارات المستخدمين الحقيقيين على الجوال والحاسب كل على حدة. والمهم أن أرقام Lighthouse على جهازي قياس مختبر، أما الحكم فبيانات المستخدمين الحقيقيين RUM، وجهاز المستخدم المتوسط عندنا هاتف متوسط وليس حاسباً حديثاً.
أسئلة المتابعة
- لماذا INP لا يظهر في Lighthouse بنفس الشكل؟
- كيف تجمع هذه المقاييس من المستخدمين بنفسك؟ (مكتبة web-vitals)
علامات تحذير: يعتبر نتيجة 100 في Lighthouse على حاسبه دليلاً كافياً.
المقال الذي يشرح الموضوع: تتبع الطلب البطيء من المتصفح إلى قاعدة البيانات.
27. كيف تعرض صورة كبيرة دون أن تبطئ الصفحة على الجوال؟
مبتدئ ماذا يختبر: صيغ الصور والأحجام المتجاوبة والتحميل الكسول.
إجابة المبتدئ
نضغط الصورة ونستخدم loading="lazy".
إجابة الخبير
أختار الصيغة (AVIF أو WebP مع بديل)، وأرسل حجماً يناسب الشاشة بـ srcset وsizes حتى لا يحمل الهاتف صورة بعرض 3000 بكسل، وأكتب width وheight لحجز المساحة. وloading="lazy" للصور تحت الشاشة الأولى فقط، أما صورة LCP في أعلى الصفحة فالتحميل الكسول يؤخرها، والصحيح أن أعطيها fetchpriority="high". وأقدمها من CDN بكاش طويل واسم فيه الهاش.
أسئلة المتابعة
- لماذا يضر
loading="lazy"صورة البطل Hero Image؟ - ما الفرق بين AVIF وWebP في الحجم والتوافق ووقت الترميز؟
علامات تحذير: يضع التحميل الكسول على كل الصور.
مقالات تشرح الموضوع: صيغ الصور: كيف تختار الصيغة المناسبة لموقعك، وكيف تعمل شبكة توصيل المحتوى CDN وما الذي تحفظه.
28. حجم JavaScript في الصفحة الأولى 2 ميجابايت، كيف تخفضه؟
متوسط ماذا يختبر: تحليل الحزمة Bundle وتقسيم الكود Code Splitting.
إجابة المبتدئ
نستخدم Minify ونحذف المكتبات غير المستخدمة.
إجابة الخبير
أبدأ بأداة تحليل الحزمة لأرى ما بداخلها، وغالباً أجد مكتبة كاملة استوردت لأجل دالة واحدة (مكتبة تواريخ بكل لغاتها، أو مكتبة أيقونات كاملة)، أو نفس المكتبة بإصدارين. ثم أقسم الكود حسب الصفحة بالاستيراد الديناميكي import() حتى لا تحمل صفحة الدخول كود لوحة التحكم، وأؤجل ما لا يلزم للتفاعل الأول (المحرر، والخرائط، والرسوم البيانية). ولاحظ أن الحجم المضغوط على الشبكة ليس هو التكلفة كلها: 2 ميجابايت غير مضغوطة يجب أن يحللها الهاتف وينفذها، وهذا وقت معالج على الخيط الرئيسي.
أسئلة المتابعة
- ما هو Tree Shaking، ولماذا لا يعمل مع بعض المكتبات؟
- كيف تمنع زيادة الحجم في المستقبل؟ (ميزانية أداء في CI)
علامات تحذير: يقيس الحجم المضغوط وحده ويتجاهل وقت التنفيذ.
مقالات تشرح الموضوع: أدوات البناء Build Tools وماذا يحدث عندما تبني مشروعك، وكيف يعمل ضغط البيانات Compression؟ من Huffman إلى Zstd.
29. نشرت إصداراً جديداً وبعض المستخدمين يرون صفحة بيضاء، لماذا؟
متوسط ماذا يختبر: كاش الملفات الثابتة والإصدارات المختلطة.
إجابة المبتدئ
المتصفح يحتفظ بالملفات القديمة، فنطلب من المستخدم تحديث الصفحة.
إجابة الخبير
السيناريو الشائع: صفحة HTML قديمة في الكاش تطلب app.3f9a.js الذي حذف من السيرفر بعد النشر، أو صفحة مفتوحة منذ ساعات تحمل جزءاً ديناميكياً Chunk لم يعد موجوداً. والحل: HTML بكاش قصير أو no-cache، والملفات الثابتة بأسماء فيها الهاش وكاش طويل، وإبقاء ملفات الإصدار السابق فترة بعد النشر، ومعالجة فشل تحميل الجزء الديناميكي بإعادة تحميل الصفحة مرة واحدة. ومع Service Worker انتبه أكثر، لأنه قد يقدم النسخة القديمة لأيام.
أسئلة المتابعة
- كيف تخبر المستخدم أن هناك إصداراً جديداً دون أن تقطع عمله؟
- لماذا قد يكون Service Worker السبب؟
علامات تحذير: يحذف الملفات القديمة فور النشر.
مقالات تشرح الموضوع: كيف تعمل شبكة توصيل المحتوى CDN وما الذي تحفظه، والتخزين المؤقت Caching بالتفصيل: الاستراتيجيات والإبطال.
الأمن في الواجهة
30. كيف يحدث XSS في تطبيق React أو Vue رغم أنهما يهربان النص؟
مبتدئ ماذا يختبر: حقن السكربت XSS والأماكن التي لا يحميك فيها الإطار.
إجابة المبتدئ
عند استخدام dangerouslySetInnerHTML أو v-html مع بيانات من المستخدم.
إجابة الخبير
الإطار يهرب النص في المحتوى والسمات العادية، ولكنه لا يحميك في: HTML الخام (dangerouslySetInnerHTML وv-html وinnerHTML)، وروابط href تبدأ بـ javascript: من بيانات المستخدم، وeval ومشتقاته، وقوالب الخادم التي تحقن بيانات داخل <script>، والمكتبات الخارجية التي تعدل ال DOM. وعندما أحتاج HTML من المستخدم (محرر نصوص) أنظفه بمكتبة مثل DOMPurify، وأضيف CSP كطبقة ثانية تمنع تنفيذ السكربتات الغريبة حتى لو نجح الحقن.
أسئلة المتابعة
- كيف تتحقق من أن الرابط الذي أدخله المستخدم آمن؟
- لماذا نخرج التوكن من
localStorageعندما نتحدث عن XSS؟
علامات تحذير: يعتقد أن الإطار يحميه في كل الحالات.
مقالات تشرح الموضوع: OWASP Top 10 للمبتدئين: أخطر عشر مشكلات في تطبيقات الويب، وثغرات الويب بالتجربة: تطبيق نكسره ثم نصلحه.
31. كيف تضيف CSP صارمة لتطبيق قائم دون أن تكسره؟
خبير ماذا يختبر: سياسة أمن المحتوى Content Security Policy عملياً.
إجابة المبتدئ
نضيف ترويسة Content-Security-Policy ونحدد المصادر المسموحة للسكربتات.
إجابة الخبير
قائمة النطاقات المسموحة يتجاوزها المهاجم غالباً إذا كان فيها CDN عام، لذلك الأفضل سياسة مبنية على Nonce أو Hash مع strict-dynamic، وتمنع unsafe-inline وunsafe-eval. وللتطبيق القائم أبدأ بـ Content-Security-Policy-Report-Only مع جمع التقارير لأسابيع، فأرى ما سينكسر (سكربتات داخلية، ومعالجات onclick في HTML، وأدوات التحليل) وأصلحه، ثم أفعل السياسة. وأضيف frame-ancestors لمنع تضمين الصفحة في إطار Clickjacking، وobject-src 'none' وbase-uri.
أسئلة المتابعة
- لماذا لا تكفي قائمة نطاقات تحتوي CDN عاماً؟
- كيف تعمل Nonce مع صفحات تحفظ في CDN؟
علامات تحذير: يضع unsafe-inline ليعمل كل شيء ويعتبرها حماية.
المقال الذي يشرح الموضوع: HSTS وCSP: ترويسات الأمان التي يحتاجها كل تطبيق.
32. ما هو CORS، ولماذا لا يحمي ال API من الطلبات؟
متوسط ماذا يختبر: فهم أن CORS قيد في المتصفح على قراءة الرد، وليس جداراً للسيرفر.
إجابة المبتدئ
CORS يمنع موقعاً آخر من استدعاء ال API، ونحله بإضافة Access-Control-Allow-Origin: *.
إجابة الخبير
المتصفح يمنع سكربت موقع من قراءة رد موقع آخر إلا إذا سمح السيرفر بترويسات CORS، وهذا يحمي المستخدم وليس السيرفر، لأن curl أو أي سيرفر يرسل الطلب ويقرأ الرد دون أي قيد. وطلب DELETE أو JSON يمر بطلب تمهيدي Preflight بـ OPTIONS، أما النموذج البسيط فيصل للسيرفر دون أي سؤال، لذلك CORS ليس حماية من CSRF. والخطأ الخطير هو أن يعكس السيرفر أي Origin يأتيه مع Access-Control-Allow-Credentials: true، فيستطيع أي موقع قراءة بيانات المستخدم المسجل.
أسئلة المتابعة
- لماذا لا يمكن استخدام
*مع Credentials؟ - متى لا يرسل المتصفح Preflight؟
علامات تحذير: يعتبر CORS آلية مصادقة.
المقال الذي يشرح الموضوع: CORS: لماذا يرفض المتصفح طلبك، وكيف تسمح به بأمان.
33. ما فائدة SameSite وHttpOnly وSecure على الكوكيز؟
متوسط ماذا يختبر: تأمين الجلسة في المتصفح وCSRF.
إجابة المبتدئ
HttpOnly يمنع JavaScript من قراءتها، وSecure لإرسالها عبر HTTPS فقط.إجابة الخبير
HttpOnly يمنع سرقة الجلسة بسكربت محقون (ولكنه لا يمنع السكربت من إرسال طلبات باسم المستخدم)، وSecure يمنع إرسالها عبر http، وSameSite=Lax يمنع إرسالها في الطلبات عبر المواقع إلا في التنقل العادي بـ GET، فيحمي من أغلب CSRF، وStrict أقوى ولكنه يخرج المستخدم القادم من رابط في بريد. وأضيف البادئة __Host- حتى لا يكتب نطاق فرعي مخترق فوق الكوكي. وتبقى حماية CSRF الصريحة (توكن أو التحقق من Origin) للعمليات الحساسة، لأن النطاقات الفرعية تعتبر نفس الموقع Same-Site.أسئلة المتابعة
- ما الفرق بين Same-Site وSame-Origin؟
- لماذا قد يكون نطاق فرعي يستضيف محتوى المستخدمين خطراً على الكوكيز؟
علامات تحذير: يخلط بين HttpOnly وSecure.
المقال الذي يشرح الموضوع: كيف يتذكرك الموقع بعد تسجيل الدخول؟ الجلسات وJWT.
التخزين في المتصفح
34. أين تحفظ بيانات في المتصفح: localStorage أم IndexedDB أم الكوكيز؟
مبتدئ ماذا يختبر: معرفة أنواع التخزين وحدودها.
إجابة المبتدئ
الكوكيز للجلسة، وlocalStorage للإعدادات الصغيرة، وIndexedDB للبيانات الكبيرة.
إجابة الخبير
الكوكيز ترسل مع كل طلب للسيرفر، فهي للجلسة فقط وحجمها صغير. وlocalStorage متزامن يحجب الخيط الرئيسي، ويحفظ نصوصاً فقط، ويقرأه أي سكربت في الصفحة، لذلك للتفضيلات الصغيرة غير الحساسة. وIndexedDB غير متزامن، ويحفظ كائنات وملفات بأحجام كبيرة، ويعمل من Web Worker، فهو للتطبيقات التي تعمل دون اتصال. وكل التخزين قد يمسح: المستخدم يمسح البيانات، والمتصفح يمسح عند امتلاء المساحة، وSafari يمسح بيانات المواقع التي لم يزرها المستخدم مدة، لذلك لا يكون المتصفح المكان الوحيد لبيانات مهمة.
أسئلة المتابعة
- ماذا يفعل
navigator.storage.persist()؟ - لماذا يكون
sessionStorageمختلفاً بين تبويبين لنفس الموقع؟
علامات تحذير: يحفظ توكنات الوصول الطويلة أو بيانات حساسة في localStorage.
المقال الذي يشرح الموضوع: كيف يتذكرك الموقع بعد تسجيل الدخول؟ الجلسات وJWT.
35. كيف تجعل تطبيق الويب يعمل دون اتصال ويحفظ ما كتبه المستخدم؟
خبير ماذا يختبر: Service Worker والتخزين المحلي والمزامنة.
إجابة المبتدئ
نستخدم Service Worker ليحفظ الملفات ويعرضها عند انقطاع الإنترنت.
إجابة الخبير
Service Worker يعترض الطلبات ويقدم الملفات من الكاش (الواجهة)، والبيانات في IndexedDB، والتعديلات التي تحدث دون اتصال تحفظ في طابور محلي يرسل عند عودة الاتصال، مع مفتاح Idempotency لكل تعديل حتى لا يتكرر. والصعب ليس العرض وإنما التعارض: المستخدم عدل نفس السجل من جهازين، فمن يفوز؟ هنا أحتاج رقم إصدار لكل سجل والسيرفر يرفض التعديل المبني على إصدار قديم، وواجهة تعرض التعارض. وهذا موضوع التمرين الثالث في مقال أسئلة الجوال.
أسئلة المتابعة
- ما استراتيجيات الكاش في Service Worker، ومتى تستخدم كل منها؟
- كيف تحدث Service Worker دون أن يعمل إصداران معاً؟
علامات تحذير: يرى العمل دون اتصال مشكلة كاش للملفات فقط.
مقالات تشرح الموضوع: التخزين المؤقت Caching بالتفصيل: الاستراتيجيات والإبطال، وIdempotency: كيف تجعل العملية آمنة عند تكرارها.
الاختبارات في الواجهة
36. ماذا تختبر في مكون الواجهة، وكيف؟
مبتدئ ماذا يختبر: اختبار السلوك وليس التفاصيل الداخلية.
إجابة المبتدئ
نختبر أن المكون يعرض البيانات الصحيحة وأن الأزرار تعمل.
إجابة الخبير
أختبر ما يراه المستخدم ويفعله: أبحث عن العناصر بدورها واسمها (getByRole('button', { name: 'حفظ' })) وليس بأسماء ال Classes، وأضغط وأكتب كما يفعل المستخدم، وأتحقق من النتيجة الظاهرة. وهذا يجعل الاختبار يصمد أمام إعادة الهيكلة، ويختبر سهولة الوصول كأثر جانبي، لأن الزر الذي لا يجده الاختبار بدوره لا يجده قارئ الشاشة أيضاً. ولا أختبر الحالة الداخلية أو عدد مرات الرسم، وأحاكي الشبكة على مستوى الطلب (Mock Service Worker مثلاً) وليس دوال الجلب.
أسئلة المتابعة
- لماذا تفضل البحث بالدور على
data-testid؟ - متى تكون لقطات Snapshot Tests مفيدة ومتى تضر؟
علامات تحذير: يختبر تفاصيل التنفيذ فينكسر الاختبار مع كل تغيير غير مؤثر.
مقالات تشرح الموضوع: أنواع الاختبارات للمبتدئين: من Unit إلى E2E مع Playwright، واختبار سهولة الوصول Accessibility لموقع عربي.
37. اختبار E2E ينجح على جهازك ويفشل أحياناً في CI، ما الأسباب الشائعة؟
متوسط ماذا يختبر: الاختبارات الهشة في المتصفح.
إجابة المبتدئ
ربما الصفحة أبطأ في CI، فنزيد وقت الانتظار.
إجابة الخبير
الانتظار الثابت waitForTimeout(2000) هو أول سبب، والصحيح انتظار حالة (ظهور عنصر، أو انتهاء طلب) وهذا ما تفعله أدوات مثل Playwright تلقائياً مع ال Locators. ثم البيانات المشتركة بين الاختبارات (اختبار يحذف ما يحتاجه آخر)، والاعتماد على الترتيب، والحركات Animations، والخطوط والمنطقة الزمنية واللغة المختلفة في CI، والخدمات الخارجية الحقيقية. وأحفظ التتبع Trace والفيديو عند الفشل لأرى ما حدث بدلاً من التخمين، وأجعل كل اختبار ينشئ بياناته.
أسئلة المتابعة
- كيف تفتح Trace لاختبار فشل في CI؟
- لماذا يجب ضبط المنطقة الزمنية واللغة في إعدادات الاختبار؟
علامات تحذير: يحل كل فشل بزيادة المهلة.
مقالات تشرح الموضوع: أنواع الاختبارات للمبتدئين: من Unit إلى E2E مع Playwright، واختبار تطبيقات الجوال مع Appium وMaestro.
38. كيف تكتشف أن تعديلاً في CSS كسر الواجهة العربية دون أن ينتبه أحد؟
خبير ماذا يختبر: الاختبار البصري Visual Regression واختبار الاتجاهين.
إجابة المبتدئ
نراجع الصفحات يدوياً قبل النشر.
إجابة الخبير
اختبار لقطات الشاشة Visual Regression يقارن الصفحة بلقطة معتمدة ويفشل عند الفرق، وأشغله على الاتجاهين وعدة عروض شاشة، مع تثبيت ما يتغير بطبيعته (التواريخ، والبيانات العشوائية، والحركات، والخطوط المحملة) حتى لا يصبح الاختبار ضجيجاً. وهو مكلف في الصيانة، لذلك أضعه على المكونات الأساسية في نظام التصميم وعلى صفحات قليلة مهمة، وليس على كل شيء. ومع الواجهة العربية أختبر نصوصاً طويلة حقيقية، لأن العربية قد تكون أطول أو أقصر من الإنجليزية وتكسر الأزرار.
أسئلة المتابعة
- كيف تمنع اختلاف خطوط النظام بين أجهزة المطورين وCI من إفشال الاختبار؟
- من يوافق على اللقطة الجديدة عندما يكون التغيير مقصوداً؟
علامات تحذير: لا يختبر الواجهة العربية إلا يدوياً.
مقالات تشرح الموضوع: اختبار سهولة الوصول Accessibility لموقع عربي، وأدوات ضمان الجودة QA: أيها يناسب فريقك؟.
أدوات البناء
39. ماذا تفعل أداة الحزم Bundler بكودك قبل أن يصل للمتصفح؟
متوسط ماذا يختبر: فهم خط البناء وليس حفظ اسم الأداة.
إجابة المبتدئ
تجمع الملفات في ملف واحد وتصغرها.
إجابة الخبير
تبدأ من نقطة الدخول وتبني شجرة الاستيراد، وتحول ما لا يفهمه المتصفح (TypeScript وJSX وأحدث صيغ اللغة إذا لزم)، وتحذف الكود غير المستخدم Tree Shaking، وتقسم الناتج إلى أجزاء، وتصغره، وتضيف الهاش إلى أسماء الملفات، وتنتج Source Maps. وفي وضع التطوير تعمل أدوات مثل Vite بطريقة مختلفة: تقدم الملفات كوحدات ES Modules مباشرة دون تجميع، وتستخدم esbuild للتحويل السريع، ثم تجمع للإنتاج. ومعرفة هذا تفسر أخطاء مثل «يعمل في التطوير ويفشل في الإنتاج».
أسئلة المتابعة
- لماذا قد يعمل الكود في وضع التطوير ويفشل بعد البناء؟
- ماذا تفعل بال Source Maps في الإنتاج؟
علامات تحذير: يعامل أداة البناء كصندوق أسود ولا يعرف ما تنتجه.
المقال الذي يشرح الموضوع: أدوات البناء Build Tools وماذا يحدث عندما تبني مشروعك.
40. كيف تتعامل مع ألف حزمة npm في مشروع الواجهة؟
خبير ماذا يختبر: أمن سلسلة التوريد Supply Chain والاعتماديات.
إجابة المبتدئ
نشغل npm audit ونحدث الحزم التي فيها ثغرات.
إجابة الخبير
أثبت الإصدارات بملف القفل Lockfile وأستخدم npm ci في CI، وأفحص الثغرات المعروفة SCA مع فهم أن أغلب التنبيهات في حزم التطوير لا تصل للمتصفح، وأحدث بانتظام بأداة مثل Renovate بدفعات صغيرة. وأخاف أكثر من الحزم الضارة: اسم قريب من حزمة مشهورة Typosquatting، وسكربتات postinstall التي تعمل على جهاز المطور وفي CI بكل صلاحياتها، وحساب صيانة مخترق ينشر إصداراً خبيثاً. وقبل إضافة حزمة أسأل: هل أحتاجها فعلاً، ومن يصونها، وكم حزمة تجلب معها؟
أسئلة المتابعة
- ماذا يحدث عند تشغيل
npm installلحزمة فيهاpostinstall؟ - لماذا
npm ciوليسnpm installفي CI؟
علامات تحذير: يضيف حزمة لكل دالة صغيرة دون تفكير.
مقالات تشرح الموضوع: أمن سلسلة التوريد البرمجية Software Supply Chain للمبتدئين، وSCA: كيف تجد الثغرات المعروفة في مكتبات مشروعك.
العرض على السيرفر SSR والعرض في المتصفح CSR
41. متى تختار العرض على السيرفر أو التوليد المسبق أو العرض في المتصفح؟
متوسط ماذا يختبر: الموازنة بين SSR وSSG وCSR حسب نوع الصفحة.
إجابة المبتدئ
SSR أفضل لمحركات البحث، وCSR أفضل للتطبيقات التفاعلية.
إجابة الخبير
أختار لكل صفحة وليس للتطبيق كله: التوليد المسبق SSG للمحتوى الذي يتغير قليلاً (المدونة، والتوثيق) لأنه HTML جاهز من CDN، والعرض على السيرفر SSR للصفحات العامة التي تتغير لكل طلب أو مستخدم ويهمها أول عرض ومحركات البحث، والعرض في المتصفح CSR للوحات التحكم خلف تسجيل الدخول حيث لا يهم محرك البحث. وSSR ليس مجانياً: سيرفر يجب تشغيله وتوسيعه، وزمن أول بايت يعتمد على أبطأ طلب بيانات، وال Hydration تكلف وقتاً على الهاتف، لذلك ظهرت أساليب مثل التحديث التدريجي ISR وال Streaming ومكونات السيرفر.
أسئلة المتابعة
- ما هي ال Hydration ولماذا تؤثر على INP؟
- كيف يجعل Streaming الصفحة تظهر قبل انتهاء كل البيانات؟
علامات تحذير: يختار SSR لكل شيء أو CSR لكل شيء.
مقالات تشرح الموضوع: Headless CMS: متى تحتاجه، وهل تختار Strapi أم Directus؟، وكيف تعمل شبكة توصيل المحتوى CDN وما الذي تحفظه.
42. ظهر تحذير Hydration mismatch في الإنتاج، ما أسبابه؟
خبير ماذا يختبر: فهم أن HTML السيرفر يجب أن يطابق أول رسم في المتصفح.
إجابة المبتدئ
السيرفر والمتصفح رسما شيئاً مختلفاً، فنعطل SSR لهذا المكون.
إجابة الخبير
الإطار يتوقع أن يطابق أول رسم في المتصفح HTML الذي أرسله السيرفر، فيربط الأحداث به بدلاً من بنائه، وأي اختلاف يجعله يرمي جزءاً ويعيد رسمه أو يترك الصفحة غير متسقة. والأسباب الشائعة: التاريخ والوقت (السيرفر في UTC والمتصفح في الرياض، وهذا يكثر عندنا)، والقيم العشوائية والمعرفات المولدة، والقراءة من window أو localStorage أثناء الرسم، وتنسيق الأرقام بلغة مختلفة (toLocaleString بين بيئتين)، وHTML غير صالح يصلحه المتصفح مثل div داخل p. والحل أن يكون أول رسم حتمياً، والقيم الخاصة بالمتصفح تعرض بعد التحميل.
أسئلة المتابعة
- كيف تعرض «منذ 5 دقائق» دون اختلاف بين السيرفر والمتصفح؟
- لماذا قد يختلف تنسيق الرقم العربي بين Node.js والمتصفح؟
علامات تحذير: يعطل SSR كلما ظهر التحذير دون فهم السبب.
المقال الذي يشرح الموضوع: التاريخ والوقت للمطورين ومديري الأنظمة.
أنظمة التصميم Design Systems
43. ما هي رموز التصميم Design Tokens، ولماذا تحتاجها؟
متوسط ماذا يختبر: بناء واجهة متسقة قابلة للتغيير.
إجابة المبتدئ
متغيرات للألوان والخطوط والمسافات نستخدمها في كل المكونات.
إجابة الخبير
الرموز قرارات التصميم بأسماء ثابتة، وأفضلها بطبقتين: رموز أولية (blue-600) ورموز بمعنى (color-action-primary)، والمكونات تستخدم ذات المعنى فقط، فيصبح الوضع الليلي أو علامة تجارية ثانية تغييراً في طبقة الربط لا في كل مكون. وتصدر من مصدر واحد إلى CSS Custom Properties وإلى تطبيقات الجوال، فيبقى اللون نفسه في المنصات كلها. ومع العربية أضيف رموزاً للطباعة خاصة بها: حجم الخط العربي غالباً يحتاج أكبر قليلاً، وارتفاع السطر أكبر للحروف والتشكيل.
أسئلة المتابعة
- كيف تبني الوضع الليلي بالرموز دون تكرار كل مكون؟
- لماذا لا تستخدم المكونات الرموز الأولية مباشرة؟
علامات تحذير: يكتب الألوان مباشرة في كل ملف.
المقال الذي يشرح الموضوع: مبادئ التصميم Design Principles بأمثلة قبل وبعد.
44. كيف تصمم مكون زر يستخدمه عشرون فريقاً؟
خبير ماذا يختبر: تصميم واجهة برمجية API للمكون تصمد مع الوقت.
إجابة المبتدئ
نعطيه Props للون والحجم والنص، ونضيف ما يطلبه الفرق.
إجابة الخبير
مكون مشترك واجهته عقد مثل أي API: أقل عدد من الخيارات بأسماء معنى (variant="danger" وليس color="red")، ويقبل كل سمات العنصر الأصلي ويمررها، ويدعم ref، ويظهر كرابط عندما يحتاج التنقل (Polymorphic أو asChild) دون أن يصبح div. وسهولة الوصول مبنية فيه (التركيز الظاهر، وحالة التحميل مع aria-busy، والأيقونة دون نص تحتاج اسماً إلزامياً). ولكل تغيير كاسر إصدار رئيسي ودليل ترحيل، لأن عشرين فريقاً لن يحدثوا في نفس اليوم. وكل خيار يطلبه فريق واحد أسأل عنه: هل هو نمط عام أم استثناء يبقى عند الفريق؟
أسئلة المتابعة
- فريق يريد زراً بلون غير موجود في النظام، ماذا تفعل؟
- كيف تمنع زر أيقونة دون اسم في وقت الترجمة؟
علامات تحذير: يضيف Prop لكل طلب حتى يصبح للمكون أربعون خياراً.
مقالات تشرح الموضوع: أنماط التصميم Design Patterns التي تقابلها فعلاً، وكيف تصمم API لا تضطر إلى كسره بعد سنة.
التشخيص بأدوات المطور DevTools
45. التطبيق يبطؤ كلما بقي مفتوحاً ساعات، كيف تجد التسرب؟
خبير ماذا يختبر: تسرب الذاكرة Memory Leak في تطبيقات الصفحة الواحدة وأدوات قياسه.
إجابة المبتدئ
نفتح تبويب Memory ونرى استهلاك الذاكرة.
إجابة الخبير
أكرر عملية واحدة (فتح صفحة وإغلاقها عشر مرات) ثم آخذ لقطة Heap Snapshot قبل وبعد وأقارن، وأبحث عن الكائنات التي يزيد عددها مع كل تكرار، وخاصة عناصر DOM المنفصلة Detached التي حذفت من الصفحة وما زال كود يحتفظ بها. والأسباب الشائعة: مستمع حدث على window لم يزل عند إزالة المكون، ومؤقت setInterval لم يوقف، واشتراك في مخزن أو WebSocket دون تنظيف، وكاش في متغير عام يكبر دون حد. وأتأكد من الإصلاح بنفس التجربة.
أسئلة المتابعة
- ما هي عناصر DOM المنفصلة، ولماذا تبقى في الذاكرة؟
- ما الفرق بين Shallow Size وRetained Size؟
علامات تحذير: يقترح إعادة تحميل الصفحة دورياً كحل.
المقال الذي يشرح الموضوع: الذاكرة للمبرمجين: كيف تعمل ال Stack وال Heap.
46. الصفحة بطيئة، ماذا تنظر في تبويب Network أولاً؟
مبتدئ ماذا يختبر: قراءة الشلال Waterfall والتمييز بين بطء الشبكة والسيرفر.
إجابة المبتدئ
أرى الطلبات البطيئة وحجم الملفات.
إجابة الخبير
أرتب حسب الوقت وأفتح الطلب البطيء وأقرأ التوقيت: إذا كان الوقت في انتظار أول بايت TTFB فالسيرفر بطيء، وإذا كان في التنزيل فالحجم كبير، وإذا كان في الطابور Queueing فالمتصفح ينتظر اتصالاً متاحاً. وأنظر إلى الشلال كاملاً: طلبات متتابعة كل واحد ينتظر الذي قبله (الصفحة تجلب المستخدم ثم إعداداته ثم بياناته) هي غالباً المشكلة الأكبر، والحل جلبها بالتوازي أو في طلب واحد. وأفعل تقليل سرعة الشبكة والمعالج لأرى ما يراه مستخدم بهاتف متوسط.
أسئلة المتابعة
- ماذا تعني الطلبات التي حالتها
(disk cache)أو304؟ - كيف تنسخ طلباً من المتصفح وتعيده في الطرفية؟ (Copy as cURL)
علامات تحذير: ينظر إلى الحجم وحده ويتجاهل التتابع.
مقالات تشرح الموضوع: تتبع الطلب البطيء من المتصفح إلى قاعدة البيانات، وHTTP وHTTPS ببساطة: ماذا يحدث بين الرابط والصفحة.
مراجعة كود الذكاء الاصطناعي
47. أداة الذكاء الاصطناعي ولدت مكوناً كاملاً يعمل، ماذا تراجع فيه؟
متوسط ماذا يختبر: الحكم على كود يعمل في المثال ويفشل في الحالات الحقيقية.
إجابة المبتدئ
أتأكد أنه يعمل ويشبه التصميم، وأرتب الكود.
إجابة الخبير
أفتحه بلوحة المفاتيح وحدها وبقارئ الشاشة، لأن الكود المولد كثيراً ما يصنع أزراراً من div ويهمل التسميات، ثم بالعربية واتجاه RTL ونص طويل، ثم بحالات الشبكة: تحميل بطيء وخطأ واستجابة فارغة وطلبات متداخلة. وأبحث عن innerHTML مع بيانات المستخدم، وعن تأثيرات دون تنظيف، وعن مكتبات أضافتها الأداة لشيء بسيط. والقاعدة أنني لا أدمج ما لا أستطيع شرح كل سطر فيه.
أسئلة المتابعة
- الأداة استخدمت دالة متصفح لا يدعمها Safari، كيف كنت ستكتشف ذلك؟
- كيف تجعل الفحص الآلي يمسك هذه الأخطاء قبل المراجعة؟
علامات تحذير: يقبل المكون لأنه يبدو مطابقاً للتصميم.
مقالات تشرح الموضوع: اختبارات يكتبها الذكاء الاصطناعي: متى تثق بها؟، واختبار سهولة الوصول Accessibility لموقع عربي.
تمارين عملية للمقابلة
فيما يلي ثلاثة تمارين شغلناها فعلاً: الأول والثالث في Node.js 24.21.0 داخل Docker Image الرسمية node:24-alpine، والثاني في Chromium 156 عبر Playwright 1.64.0 داخل Docker Image الرسمية mcr.microsoft.com/playwright:v1.64.0-noble، والمخرجات المنشورة هي المخرجات الحقيقية. أعط المرشح الكود دون المخرج في التمرين الأول، ومع المخرج في الثاني والثالث، واطلب منه أن يشرح ثم يصلح، والإجابة في الزر أسفل كل تمرين.
docker run --rm -v "$PWD:/w" -w /w node:24-alpine node order.js
docker run --rm -v "$PWD:/w" -w /w node:24-alpine node search.mjs
docker run --rm --ipc=host -v "$PWD:/w" -w /w mcr.microsoft.com/playwright:v1.64.0-noble \
sh -c 'cd /tmp && npm i [email protected] && cp /w/*.mjs . && node main-thread.mjs'التمرين الأول: بأي ترتيب تطبع الأسطر؟
اطلب من المرشح أن يكتب ترتيب الأسطر العشرة على الورق قبل أي تشغيل، ثم يشرح سبب كل موقع:
// order.js: write down the order of the lines before you run it
console.log("1 start");
setTimeout(() => console.log("2 timeout 0"), 0);
Promise.resolve()
.then(() => console.log("3 then A"))
.then(() => console.log("4 then B"));
queueMicrotask(() => console.log("5 microtask"));
(async () => {
console.log("6 async body");
await null;
console.log("7 after await");
})();
setTimeout(() => {
console.log("8 timeout 2");
Promise.resolve().then(() => console.log("9 then inside timeout"));
}, 0);
console.log("10 end");والصورة التالية تبين الطوابير التي يمر بها هذا الكود:

الإجابة
والمخرج في Node.js وفي Chromium (شغلنا نفس الملف في الاثنين عبر Playwright) كان متطابقاً:
1 start
6 async body
10 end
3 then A
5 microtask
7 after await
4 then B
2 timeout 0
8 timeout 2
9 then inside timeout
في المخرج أعلاه لاحظ التالي:
- السكربت نفسه مهمة واحدة، فكل الكود المتزامن ينفذ أولاً: السطر 1، ثم جسم الدالة غير المتزامنة حتى أول
await(السطر 6، وهذا ما يفوته كثيرون لأنهم يظنون أن الدالةasyncكلها تؤجل)، ثم السطر 10. - بعد انتهاء المهمة تفرغ المهام الصغرى بترتيب دخولها:
then Aثمmicrotaskثم ما بعدawait(السطر 7)، ثمthen Bلأنها لم تدخل الطابور إلا بعد أن نفذتthen A. - بعد ذلك فقط تأتي المؤقتات كمهام منفصلة، وبعد المؤقت الأول (السطر 2) لا توجد مهام صغرى، وبعد المؤقت الثاني (السطر 8) تنفذ المهمة الصغرى التي أضافها (السطر 9) قبل أي مهمة أخرى.
والمرشح الخبير يضيف من نفسه: لو كانت سلسلة then لا تنتهي لما وصلنا إلى السطر 2 أبداً ولتجمدت الصفحة، وفي Node.js يأتي process.nextTick قبل وعود Promise، وترتيب setTimeout مع setImmediate في Node.js ليس مضموناً عندما يستدعيان من الكود الرئيسي. والمرجع في ذلك شرح MDN لحلقة الأحداث.
التمرين الثاني: زر لا يستجيب
صفحة فيها زر شراء، وعمل يأخذ ثانية كاملة (بناء تقرير من آلاف الصفوف مثلاً) يبدأ بعد التحميل، والمستخدم يضغط الزر بعد 100 ملي ثانية. الصفحة تقيس كم انتظرت النقرة من لحظة حدوثها حتى بدأ المعالج، وهذا هو تأخير الإدخال Input Delay في مقياس INP، وتسجل المهام الطويلة عبر PerformanceObserver:
<!doctype html>
<html lang="ar" dir="rtl"><meta charset="utf-8">
<button id="buy" style="font-size:24px">اشتر الآن</button>
<script>
// 1000 ms of work: building a report from 200k rows, for example
function work(ms) { const end = performance.now() + ms; let x = 0; while (performance.now() < end) x++; return x; }
window.result = { clicks: [], longTasks: [] };
new PerformanceObserver((l) => l.getEntries().forEach((e) => result.longTasks.push(Math.round(e.duration))))
.observe({ type: "longtask" });
document.getElementById("buy").addEventListener("click", (e) => {
result.clicks.push(Math.round(performance.now() - e.timeStamp)); // input delay: from the click to our handler
});
// version 1: one block on the main thread
function blockOnce() { work(1000); }
// version 2: the same work in 100 chunks, yielding to the browser between them
async function chunked() {
for (let i = 0; i < 100; i++) {
work(10);
await (globalThis.scheduler?.yield?.() ?? new Promise((r) => setTimeout(r, 0)));
}
}
// version 3: the work moves to a Web Worker
function inWorker() {
const src = `onmessage = () => { const end = performance.now() + 1000; while (performance.now() < end); postMessage("done"); }`;
const w = new Worker(URL.createObjectURL(new Blob([src], { type: "text/javascript" })));
w.postMessage("go");
}
</script>
</html>والسكربت الذي يفتح الصفحة في Chromium ويضغط الزر بنفس التوقيت مع كل نسخة:
// main-thread.mjs: click the button 100 ms after the work starts, and measure how long the click waited
import { chromium } from "playwright";
const browser = await chromium.launch();
console.log("Chromium", browser.version());
for (const version of ["blockOnce", "chunked", "inWorker"]) {
const page = await browser.newPage();
await page.goto("file:///w/page.html");
const box = await page.locator("#buy").boundingBox();
await page.evaluate((v) => setTimeout(() => window[v](), 0), version);
await page.waitForTimeout(100);
await page.mouse.click(box.x + 10, box.y + 10);
await page.waitForTimeout(1300);
const r = await page.evaluate(() => window.result);
console.log(`${version.padEnd(10)} click waited ${String(r.clicks[0]).padStart(4)} ms long tasks: ${r.longTasks.length ? r.longTasks.join(", ") + " ms" : "none"}`);
await page.close();
}
await browser.close();والمخرج سوف يكون كما يلي:
Chromium 156.0.8078.4
blockOnce click waited 908 ms long tasks: 1000 ms
chunked click waited 13 ms long tasks: none
inWorker click waited 1 ms long tasks: noneاسأل المرشح: لماذا انتظرت النقرة 908 ملي ثانية مع أن المعالج نفسه لا يفعل شيئاً تقريباً؟ ولماذا اختفت المهمة الطويلة في النسختين الأخريين مع أن العمل نفسه لم يقل؟ وما الذي لا تستطيع فعله في النسخة الثالثة؟
الإجابة
معالج النقرة يعمل على نفس الخيط الرئيسي الذي ينفذ work(1000)، والمتصفح لا يقطع مهمة JavaScript في منتصفها، فالنقرة التي حدثت بعد 100 ملي ثانية بقيت في الطابور حتى انتهت المهمة، أي 900 ملي ثانية تقريباً، والمستخدم يرى زراً ميتاً. وهذا وحده يجعل INP للصفحة سيئاً، لأن الحد الجيد 200 ملي ثانية بحسب web.dev.
وفي النسخة الثانية نفس العمل (100 قطعة في كل منها 10 ملي ثانية)، ولكن بين القطع يعطي الكود المتصفح فرصة بـ scheduler.yield()، فتدخل النقرة بين قطعتين وتنتظر 13 ملي ثانية فقط، ولا توجد مهمة أطول من 50 ملي ثانية. والفرق بين scheduler.yield() وsetTimeout أن الأول يعيد الكود إلى مقدمة الطابور بعد أن يأخذ المتصفح فرصته، فلا تسبقه مهام أخرى من الصفحة، كما تشرح مدونة Chrome للمطورين، والكود يرجع إلى setTimeout في المتصفحات التي لا تدعمه.
وفي النسخة الثالثة انتقل العمل إلى Web Worker يعمل على خيط آخر، فالخيط الرئيسي حر تماماً (1 ملي ثانية). وما لا تستطيعه هناك هو الوصول إلى ال DOM، فالعامل يحسب ويرسل النتيجة بـ postMessage والخيط الرئيسي يرسمها، ونقل بيانات كبيرة بين الخيطين له تكلفة نسخ أيضاً. والمرشح الخبير يسأل قبل كل ذلك: هل يجب أن يحدث هذا العمل في المتصفح أصلاً، أم يرسله السيرفر جاهزاً؟
التمرين الثالث: نتائج البحث الخاطئة
مربع بحث يرسل طلباً مع كل حرف، والمستخدم كتب abc، ولكن الشاشة تعرض أحياناً نتائج ab. الدالة fakeFetch تحاكي سيرفراً يرد على الطلبات بأزمنة مختلفة، كما يحدث فعلاً عندما يكون البحث عن حرف واحد أثقل من البحث عن كلمة كاملة:
// search.js: a search box that shows the wrong results
// fakeFetch answers after a delay that depends on the query, like a real server under load
const delays = { a: 300, ab: 280, abc: 40 };
function fakeFetch(q, { signal } = {}) {
return new Promise((resolve, reject) => {
const t = setTimeout(() => resolve({ query: q, items: [`${q}-1`, `${q}-2`] }), delays[q]);
signal?.addEventListener("abort", () => { clearTimeout(t); reject(new DOMException("aborted", "AbortError")); });
});
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function typeSlowly(onInput) { // the user types a, ab, abc, 50 ms apart
for (const q of ["a", "ab", "abc"]) { onInput(q); await sleep(50); }
await sleep(400);
}
// version 1: what the junior wrote
let shown = null;
async function onInputV1(q) {
const res = await fakeFetch(q);
shown = res;
console.log(` v1 render "${res.query}"`);
}
// version 2: cancel the previous request and ignore anything that is not the latest
let controller = null, shown2 = null;
async function onInputV2(q) {
controller?.abort();
controller = new AbortController();
try {
const res = await fakeFetch(q, { signal: controller.signal });
shown2 = res;
console.log(` v2 render "${res.query}"`);
} catch (e) {
if (e.name !== "AbortError") throw e;
console.log(` v2 aborted "${q}"`);
}
}
console.log("typed: abc");
await typeSlowly(onInputV1);
console.log(`v1 final screen shows results for "${shown.query}"`);
await typeSlowly(onInputV2);
console.log(`v2 final screen shows results for "${shown2.query}"`);والمخرج سوف يكون كما يلي:
typed: abc
v1 render "abc"
v1 render "a"
v1 render "ab"
v1 final screen shows results for "ab"
v2 aborted "a"
v2 aborted "ab"
v2 render "abc"
v2 final screen shows results for "abc"اسأل المرشح: لماذا انتهت النسخة الأولى بنتائج ab؟ وهل يكفي تأخير الطلب Debounce لحل المشكلة؟
الإجابة
الطلبات أرسلت بالترتيب a ثم ab ثم abc، ولكنها عادت بترتيب مختلف: abc بعد 40 ملي ثانية من إرساله، ثم a بعد 300، ثم ab بعد 280 من إرساله أي في النهاية، والنسخة الأولى ترسم كل رد يصل، فآخر رد وصل هو الذي بقي على الشاشة وهو ليس آخر ما كتبه المستخدم. وهذه حالة سباق Race Condition في الواجهة، لا تظهر على جهاز المطور حيث كل الردود سريعة ومتساوية.
والنسخة الثانية تلغي الطلب السابق بـ AbortController قبل أن ترسل الجديد، فلا يرسم إلا رد آخر طلب، ويتوقف أيضاً تنزيل ما لم يعد مطلوباً. وطريقة أخرى صحيحة أن تحفظ رقماً لكل طلب وتتجاهل أي رد ليس للرقم الأخير، وهذا ما تفعله مكتبات جلب البيانات تلقائياً بمفتاح الاستعلام.
والتأخير Debounce يقلل عدد الطلبات ولكنه لا يحل المشكلة، لأن المستخدم قد يتوقف 300 ملي ثانية بين حرفين فيرسل طلبين، ويعود الأول بعد الثاني. وفي React نفس الخطأ يحدث داخل useEffect الذي يجلب البيانات دون دالة تنظيف تلغي الطلب، وهذا ما يجعل التمرين صالحاً لكل أطر العمل.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- اختر ثمانية أسئلة تقريباً لكل جلسة من مجالات مختلفة، واترك أغلب الوقت لأسئلة المتابعة.
- الفرق بين المبتدئ والخبير في Frontend يظهر في الخيط الرئيسي والعربية وسهولة الوصول والأمن، وليس في أسماء الدوال.
- التمارين العملية تكشف ما لا تكشفه الأسئلة: ترتيب التنفيذ الحقيقي، ونقرة انتظرت 908 ملي ثانية، ورد قديم يغلب الرد الجديد.
- كل سؤال مرتبط بمقال يشرح الموضوع في عرب رووت، فاستخدم الروابط لتستعد، أو أرسلها لمرشح تريد أن تساعده.
وبقية مقالات السلسلة:
دليلكيف تدير مقابلة تقنية تكشف مستوى المرشح؟كيف تبني مقابلة تقنية منظمة تفرق بين المبتدئ والخبير بأسئلة المتابعة والتقييم بالدرجات بدلاً من الانطباع.
دليلأسئلة مقابلة مطور Backend من المبتدئ إلى الخبيربنك أسئلة لمقابلة مطور Backend، لكل سؤال إجابة المبتدئ وإجابة الخبير وأسئلة متابعة، مع تمارين عملية شغلناها فعلاً.
دليلأسئلة مقابلة مطور تطبيقات الجوال من المبتدئ إلى الخبيربنك أسئلة لمقابلة مطور تطبيقات الجوال على Android وiOS، لكل سؤال إجابة المبتدئ وإجابة الخبير، مع تمارين عملية شغلناها.سجل التحديثات
- أكتوبر 2026: كتابة المقال، وتشغيل التمارين على Node.js 24.21.0 وعلى Chromium 156.0.8078.4 عبر Playwright 1.64.0 داخل Docker على Windows 11.