تخيّل معي المشهد التالي: أنت تجلس أمام شاشتك، تكتب سطرًا بسيطًا لا يتجاوز بضع كلمات:
1const res = await fetch('https://api.example.com/user');سطر واحد. نظيف. لا يوجد فيه ما يستحق التفكير. ثم تفتح تبويب الـ Network في أدوات المطور، وتجد رقمًا صغيرًا مزعجًا: 150ms.
وهنا تبدأ الحيرة. أنت تعرف تمامًا أن الكود الموجود على الخادم لا يستغرق سوى 8 ميلي ثانية. استعلام قاعدة بيانات واحد، تحويله إلى JSON، وانتهى الأمر. فأين ذهبت الـ 142 ميلي ثانية الباقية؟
الإجابة المختصرة: لم تذهب إلى أي مكان، لأنها لم تصل أصلًا إلى الكود الخاص بك.
هذه الميلي ثوانٍ استُهلكت في سلسلة من "المفاوضات" التي تجري بين جهازك والعالم الخارجي قبل أن يُسمح لطلبك بالمرور. خمس محطات، كل واحدة منها تسأل سؤالًا وتنتظر إجابة، وكل انتظار له ثمن. والمشكلة أن هذا الثمن ليس ثمنًا برمجيًا يمكنك تحسينه بإعادة كتابة دالة، بل هو ثمن فيزيائي محكوم بسرعة الضوء داخل كابل من الألياف الضوئية.
دعنا نفكك الرحلة خطوة بخطوة.
المحطة الأولى: DNS — من أنت أصلًا؟
جهازك لا يفهم شيئًا اسمه api.example.com. هذا الاسم موجود لأجلك أنت، لا لأجل الشبكة. ما تفهمه الشبكة هو عنوان IP مثل 93.184.216.34. لذا فإن أول ما يحدث هو عملية ترجمة.
والترجمة هنا ليست بحثًا في جدول واحد، بل سلسلة استعلامات متتالية تُسمى Recursive Lookup:
- جهازك يسأل الـ Resolver (غالبًا خاص بمزوّد الإنترنت، أو
1.1.1.1و8.8.8.8إن كنت غيّرت الإعدادات). - الـ Resolver يسأل خوادم الجذر (Root Servers): من يدير نطاق
.com؟ - ثم يسأل خوادم
.com: من هو الخادم المسؤول عنexample.com؟ - ثم يسأل الخادم المسؤول (Authoritative): ما عنوان
api.example.com؟
أربع محطات، كل واحدة منها ذهاب وإياب عبر الشبكة. ولو لم تكن أيٌّ من هذه الإجابات محفوظة مسبقًا في أي ذاكرة على الطريق — وهي الحالة التي يسمّيها المهندسون Cold Cache — فأنت أمام 60 إلى 120 ميلي ثانية تُدفع قبل أن تعرف حتى إلى أين سترسل أول بايت.
لماذا لا تشعر بهذا في كل مرة؟
بسبب الـ TTL (Time To Live). كل سجل DNS يأتي ومعه مدة صلاحية: "احتفظ بي في الذاكرة 300 ثانية". خلال هذه المدة، الاستعلام يُخدم من الكاش المحلي في المتصفح أو نظام التشغيل أو الـ Resolver، وينتهي في أقل من ميلي ثانية.
وهنا تقف أمام موازنة كلاسيكية يعرفها كل من عمل على بنية تحتية، إذ ما تربحه في جهة تخسره في الأخرى:
- TTL طويل (ساعة أو أكثر) = أداء ممتاز، لكن إن غيّرت خادمك فستحتاج ساعة كاملة حتى يعرف العالم بالتغيير.
- TTL قصير (30 ثانية) = مرونة عالية في التبديل والطوارئ، لكن ضغط أعلى واستعلامات أكثر.
Anycast: نفس العنوان، مواقع مختلفة
الحيلة الأذكى في هذا المجال هي Anycast. الفكرة ببساطة أن نفس عنوان الـ IP يُعلَن عنه من عشرات المواقع حول العالم عبر بروتوكول التوجيه BGP، والشبكة تتكفّل تلقائيًا بإيصالك إلى أقرب نقطة جغرافية.
هذا هو السبب في أن 1.1.1.1 يستجيب لك من القاهرة في 5 ميلي ثانية، ويستجيب لمستخدم في طوكيو في 4 ميلي ثانية، رغم أنه "نفس العنوان". أنت لا تصل إلى نفس الجهاز، بل إلى أقرب نسخة منه.
المحطة الثانية: مصافحة TCP — هل أنت مستعد للحديث؟
عرفت العنوان. رائع. لكنك لا تستطيع إرسال البيانات فورًا، لأن TCP لا يبدأ العمل قبل أن يتأكد الطرفان من أن كلًا منهما موجود ومستعد ويسمع الآخر.
المصافحة الثلاثية الشهيرة:
- SYN ← أنت: مرحبًا، أريد فتح اتصال.
- SYN-ACK ← الخادم: سمعتك، وأنا مستعد.
- ACK ← أنت: تمام، لنبدأ.
عمليًا، هذا يكلفك رحلة ذهاب وإياب كاملة (1 RTT)، لأن البيانات الفعلية يمكن إرسالها مع رسالة الـ ACK الأخيرة.
وما هو الـ RTT؟
الـ RTT هو الزمن الذي تستغرقه حزمة للوصول إلى الخادم والعودة. وهذا الرقم ليس اختياريًا، ولا يتحسّن بترقية المعالج، ولا بشراء خادم أقوى. إنه محكوم بالمسافة وسرعة الضوء داخل الألياف الضوئية (حوالي 200,000 كم/ث، أي ثلثي سرعته في الفراغ)، مضافًا إليها زمن المعالجة في كل موجّه على الطريق.
بحساب تقريبي:
- من القاهرة إلى فرانكفورت: ~40ms RTT
- من القاهرة إلى فرجينيا (شرق الولايات المتحدة): ~120ms RTT
ولاحظ هنا نقطة مهمة: لا يوجد كود في العالم يستطيع كسر هذا الرقم. يمكنك فقط تقليل عدد المرات التي تدفعه فيها.
المحطة الثالثة: TLS — أثبت أنك من تدّعي
الاتصال مفتوح، لكنه مكشوف تمامًا. أي جهاز على الطريق يستطيع قراءة كل شيء. لذلك تأتي طبقة التشفير.
هنا الفارق بين الأجيال يصبح ملموسًا جدًا:
TLS 1.2 كان يحتاج رحلتي ذهاب وإياب (2 RTT): رسالة ClientHello، ثم ServerHello مع الشهادة، ثم تبادل المفاتيح، ثم تأكيد. كل هذا قبل أن تُرسل بايتًا واحدًا من طلبك الفعلي.
TLS 1.3 أعاد تصميم العملية بالكامل واختصرها إلى رحلة واحدة (1 RTT). الحيلة أن العميل صار يخمّن مسبقًا خوارزمية تبادل المفاتيح ويرسل حصته من المفتاح مع أول رسالة، بدل أن ينتظر ليسأل. كما حُذفت الخوارزميات القديمة الضعيفة، فلم تعد هناك مفاوضات طويلة حول "ماذا ندعم كلانا؟".
و0-RTT: السرعة التي لها ثمن
إذا سبق لك التحدث مع هذا الخادم، يستطيع TLS 1.3 استخدام ما يُسمى Session Resumption لإرسال البيانات مع أول حزمة — صفر رحلات إضافية.
يبدو مثاليًا، لكن انتبه: بيانات الـ 0-RTT معرّضة لهجمات إعادة الإرسال (Replay Attacks)، لأن المهاجم قد يلتقط الحزمة ويعيد بثّها. لذلك القاعدة الذهبية: استخدمه فقط مع الطلبات التي لا يغيّر تكرارها شيئًا (Idempotent) مثل GET، ولا تضع فيه أبدًا عملية شراء أو تحويل رصيد.
حساب الرحلة حتى الآن
لنجمع الأرقام في سيناريو "اتصال جديد بالكامل" مع خادم يبعد 40ms:
| المرحلة | التكلفة |
|---|---|
| استعلام DNS (مع كاش جزئي) | ~30ms |
| مصافحة TCP | 40ms (1 RTT) |
| مصافحة TLS 1.3 | 40ms (1 RTT) |
| إرسال الطلب واستقبال أول بايت | 40ms (1 RTT) |
| المجموع | ~150ms |
ها هي الـ 150 ميلي ثانية. لم تُهدر في الكود، بل في الطقوس التي تسبق الكود. ومعالجة الخادم الفعلية (8ms) تمثل أقل من 6% من التجربة الكاملة.
المحطة الرابعة: أي إصدار من HTTP نتحدث؟
وصلنا إلى المفاوضة الرابعة، وهي التي تحدد شكل الحديث نفسه.
HTTP/1.1 ومشكلة مقدمة الطابور
في HTTP/1.1، الاتصال الواحد يخدم طلبًا واحدًا في كل مرة. ترسل طلبًا، تنتظر الرد، ثم ترسل التالي. وإذا تأخر طلب في المقدمة، يقف كل من خلفه في الانتظار. هذه هي مشكلة Head-of-Line Blocking، تمامًا كطابور في مخبز حيث الشخص الأول يطلب مئة رغيف والجميع ينتظر.
الحل الترقيعي الذي اعتمدته المتصفحات كان فتح 6 اتصالات متوازية لكل نطاق. وهذا يعني أنك تدفع تكلفة TCP + TLS ست مرات. ومن هنا وُلدت ممارسة قديمة اسمها Domain Sharding (توزيع الملفات على نطاقات فرعية متعددة) للحصول على المزيد من الاتصالات.
HTTP/2 وتعدد المسارات (Multiplexing)
جاء HTTP/2 بفكرة أنيقة: اتصال TCP واحد فقط، وبداخله عدد غير محدود من "التدفقات" (Streams) المتوازية، مع ضغط للترويسات عبر HPACK وأولويات للطلبات.
النتيجة كانت قفزة حقيقية. لكن بقيت مشكلة واحدة عنيدة: الحجب انتقل من طبقة HTTP إلى طبقة TCP نفسها. لأن TCP يضمن الترتيب، فإن ضياع حزمة واحدة يجبر النظام على إيقاف كل التدفقات حتى تصل الحزمة المفقودة — حتى تلك التدفقات التي لا علاقة لها بها إطلاقًا. على شبكة ممتازة لن تلاحظ الفرق، أما على شبكة جوال بنسبة فقد 2% فالأداء قد ينهار.
وبالمناسبة: مع HTTP/2، ممارسة Domain Sharding القديمة تحولت من تحسين إلى ضرر، لأنها تجزّئ الاتصال الواحد المُحسَّن إلى عدة اتصالات مكلفة.
HTTP/3 وQUIC: الهروب من TCP
الحل الجذري كان جريئًا: التخلي عن TCP كليًا. بروتوكول QUIC بُني فوق UDP، ونفّذ ما يحتاجه من ضمانات داخل التطبيق نفسه، بدلًا من الاعتماد على نواة نظام التشغيل.
ماذا يعطينا هذا؟
- لا حجب متقاطع بين التدفقات: كل تدفق مستقل، وضياع حزمة في تدفق لا يوقف البقية.
- مصافحة مدمجة: التشفير جزء أصيل من البروتوكول لا طبقة فوقه، فالاتصال الجديد يكتمل في 1 RTT، والمُستأنف في 0 RTT.
- انتقال الاتصال (Connection Migration): الاتصال يُعرَّف بمعرّف Connection ID لا بعنوان IP. لذا حين تخرج من المنزل وينتقل هاتفك من الواي فاي إلى شبكة الجوال، الاتصال يستمر بلا انقطاع بدل أن ينهار ويعيد كل المصافحات من الصفر.
الثمن؟ استهلاك أعلى قليلًا للمعالج، وبعض شبكات الشركات لا تزال تقيّد حركة UDP أو تحجبها، فيحدث تراجع تلقائي إلى HTTP/2.
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| طبقة النقل | TCP | TCP | QUIC فوق UDP |
| التعددية | لا | نعم | نعم |
| الحجب في طبقة النقل | موجود | موجود | غير موجود |
| مصافحة اتصال جديد | 2–3 RTT | 2–3 RTT | 1 RTT |
| تغيير الشبكة | قطع الاتصال | قطع الاتصال | انتقال سلس |
المحطة الخامسة: البيانات نفسها
بعد كل هذا، يبدأ الطلب الفعلي. وهنا يظهر مقياسان يجب ألا تخلط بينهما:
- TTFB (زمن أول بايت): وقت التفكير عند الخادم.
- Content Download: وقت نقل البيانات فعليًا.
وهنا نقطة يغفل عنها كثيرون: نقل البيانات ليس مسألة "سرعة إنترنت" فقط. TCP يبدأ بحذر عبر آلية Slow Start، فلا يرسل كل ما لديه دفعة واحدة بل يزيد المعدل تدريجيًا مع كل RTT ناجح. لذلك فإن رد JSON بحجم 500KB قد لا يُنقل في رحلة واحدة، بل يحتاج عدة جولات ذهاب وإياب لتتوسع نافذة الإرسال.
الترجمة العملية: تقليص حجم الرد من 500KB إلى 50KB قد يوفّر عليك عدة RTT كاملة، أي أضعاف ما ستوفره من تحسين استعلام قاعدة البيانات.
ماذا تفعل بهذه المعرفة؟ خطوات عملية
هذا ليس فضولًا نظريًا. إليك ما يترجَم مباشرة إلى مكاسب:
1. أعد استخدام الاتصالات — هذا أهم بند في القائمة. إن كنت تنادي API من خادم Node.js، فتأكد أن الـ Agent مفعّل عليه keepAlive. اتصال يُفتح ويُغلق مع كل طلب يعني أنك تدفع ضريبة TCP + TLS كل مرة. مجرد تفعيل تجميع الاتصالات (Connection Pooling) قد يقطع 80ms من كل نداء.
2. ابدأ المصافحة مبكرًا في الواجهة الأمامية:
1<link rel="preconnect" href="https://api.example.com" crossorigin>
2<link rel="dns-prefetch" href="https://api.example.com">بهذا تجري مصافحات DNS وTCP وTLS أثناء انشغال المستخدم بقراءة الصفحة، لا في اللحظة التي يضغط فيها الزر.
3. اجعل مصافحة TLS تتم قرب المستخدم. استخدام CDN أو Edge لا يفيد الملفات الثابتة فحسب؛ إتمام المصافحة على بُعد 10ms بدل 120ms يقلّص أغلى جزء في الرحلة، حتى لو ظل الخادم الأصلي بعيدًا.
4. قلّل عدد النطاقات، لا تزدها. ما كان تحسينًا في 2012 صار عبئًا في 2026.
5. فعّل ضغط Brotli، وخفّف حجم الرد. لا ترسل 40 حقلًا والواجهة تعرض ثلاثة.
6. ادمج الطلبات المتسلسلة. ثلاثة نداءات متتابعة يعتمد كل منها على سابقه = ثلاثة RTT مضروبة في المسافة. نداء واحد مجمّع يعني RTT واحدًا.
الخلاصة: الشبكة فيزياء، لا كود
الفكرة التي أرجو أن تبقى معك بعد هذا المقال بسيطة لكنها تغيّر أولوياتك:
زمن الاستجابة على الشبكة قيد فيزيائي، لا مشكلة برمجية. ولذلك فإن تحسين إعادة استخدام الاتصالات وتقليص حجم البيانات المنقولة يوفّر من الوقت الذي يشعر به المستخدم أكثر مما توفره إعادة هيكلة كود الخادم.
حين تقضي يومين كاملين في تحسين دالة من 12ms إلى 4ms، أنت ربحت 8 ميلي ثانية في رحلة طولها 150. أما تفعيل keepAlive في سطر واحد، أو وضع الـ API خلف خادم طرفي قريب من المستخدم، فقد يقتطع 60 أو 80 ميلي ثانية دفعة واحدة.
الكود الذي تكتبه مهم، بلا شك. لكن قبل أن يصل إليه الطلب، تكون خمس مفاوضات صامتة قد جرت وانتهت بالفعل. ومن يفهم هذه المفاوضات، يعرف أين يستحق أن يبذل جهده.
والسؤال الآن: متى كانت آخر مرة فتحت فيها تبويب Network ونظرت إلى ما يحدث قبل وصول الطلب إلى كودك؟





