التكلفة المنسيّة لإعداد الاتصال
TLS، DNS، TCP. الـ 600ms المخفية قبل ما يشتغل أي كود عندك. وين تستردّها.
جاري تحميل المقال…
TLS، DNS، TCP. الـ 600ms المخفية قبل ما يشتغل أي كود عندك. وين تستردّها.
جاري تحميل المقال…
طلب إلى خادمك يستغرق ٦٠٠ مللي ثانية. خادمك يعالجه في ٥٠. الـ ٥٥٠ الأخرى موزّعة بين TLS و DNS و TCP، كلّها تحدث قبل أن يعمل كودك. معظم كتابات الأداء تعامل هذه ككلفة ثابتة. ليست. هذا حيث يعيش الوقت فعلاً، وأين تستردّ بعضه.
طلب HTTPS بارد من هاتف على خلويّ إلى خادم في منطقة أخرى:
DNS resolution ٧٠ مللي ثانية (cache بارد)
TCP three-way handshake ٨٠ مللي ثانية (ذهاب وعودة واحد)
TLS 1.3 handshake ١٨٠ مللي ثانية (ذهاب وعودة واحد)
HTTP request ٥ مللي ثانية
معالجة الخادم يتفاوت
HTTP response ٥ مللي ثانية
────────
≥٣٥٠ مللي ثانية قبل تبادل أيّ بيانات
ذلك الرقم لاتّصال صحّيّ في ظروف متوسّطة. الشبكات الخلويّة والريفيّة تضيف ١٠٠-٣٠٠ مللي ثانية ذهاب وعودة فوقها. قسنا ١٫٤ ثانية من overhead ما-قبل-البيانات على اتّصالات حقيقيّة من مصر إلى Frankfurt خلال ساعات الذروة.
النقطة هي أنّ «هذا التطبيق بطيء» المرئيّ للمستخدم غالباً ليس شيئاً فعله كود التطبيق بشكل خاطئ. هي كلفة قول مرحباً.
ثلاث من الخطوات الأربع قابلة للتقليل.
بحث DNS هو ذهاب وعودة أو اثنان إلى محلّل recursive. التخفيفات:
نستخدم الأوّلَين. لا نستخدم الثالث لأنّنا نريد القدرة على تغيير DNS بدون تحديث تطبيق.
TCP three-way handshake هو ذهاب وعودة واحد ولا يمكن تجنّبه على اتّصال بارد. التخفيفات:
نستخدم HTTP/2 مع connection pool. HTTP/3 في قائمتنا لكنّ الـ proxy أمام API لا يدعمه بنظافة بعد.
TLS 1.3 ذهاب وعودة واحد. TLS 1.2 كان اثنَين. تأكّد أنّك على 1.3 من طرف لطرف. التحرّكات الأخرى:
استئناف الجلسة مُفعَّل افتراضيّاً في الـ stacks الحديثة. 0-RTT opt-in على معظم reverse proxies؛ افحص خاصّتك.
بترتيب تقريبيّ لكم استرددنا لكلّ ساعة عمل:
HttpClient Dart الافتراضيّ و HttpURLConnection Java يُبقيان
الاتّصالات مفتوحة. غلاف HTTP الساذج لدينا كان يُنشئ client جديد
لكلّ طلب. إعادة استخدام client واحد عبر التطبيق وفّر ذهاب وعودة TCP+TLS
كاملاً على كلّ طلب بعد الأوّل. متوسّط زمن الطلب انخفض ٢٠٠ مللي ثانية.
// قبل
Future<Response> get(String url) async {
final client = HttpClient();
// ... استخدم client
client.close();
}
// بعد
class Api {
static final _client = HttpClient(); // مشترك، عمر التطبيق
Future<Response> get(String url) async {
// ... استخدم _client
}
}حين يبدأ التطبيق، نُصدِر طلب HEAD إلى جذر API. لا ينقل بيانات،
لكن يُسخِّن DNS، يفتح اتّصال TCP، ويُكمل TLS handshake. بحلول قيام
المستخدم بأيّ شيء حقيقيّ، الاتّصال جاهز.
Future<void> warmConnection() async {
try {
await _client.head(Uri.parse('https://api.aliyosef.online/health'));
} catch (_) {
// أفضل جهد. الشبكة قد تكون معطّلة. لا تحجب الإقلاع.
}
}هذا غير مرئيّ للمستخدم لكنّه يجعل أوّل طلب حقيقيّ يشعر فوريّاً. الكلفة: طلب إضافيّ واحد لكلّ جلسة، ~٥ KB. يستحقّ.
إن كان مستخدموك في منطقة جغرافيّة واحدة وخادمك في أخرى، لا يوجد قدر من تعديل البروتوكول سينقذك من سرعة الضوء. نقلنا API لدينا من EU-West إلى ME-Central لمستخدمينا السعوديّين والسوريّين. زمن استجابة الذهاب والعودة انخفض من ٢٢٠ مللي ثانية إلى ٣٠ مللي ثانية. تلك الحركة الواحدة وفّرت وقتاً أكثر من كلّ تحسين آخر مجتمعاً.
Gzip على الردود رهان طاولة. Brotli أفضل لكنّه ليس مدعوماً عالميّاً.
كلاهما يقلّل بايتات السلك، ممّا يقلّل كلفة multi-RTT على الروابط
البطيئة. اضبط Accept-Encoding: gzip, br على العميل واضبط الخادم
لضغط الردود فوق ~١ KB.
الإصلاح الأذكى هو عدم إجراء الذهاب والعودة في المقام الأوّل، وهي النقطة الكاملة لـ التصميم offline-first. الطلب الأوّل بعد الإقلاع يمكن الإجابة عليه من cache محلّيّ؛ الشبكة تُستخدم فقط لتحديث الـ cache، في الخلفيّة.
ثلاثة تحسينات اعتبرناها ورفضناها.
يوفّر ذهاب وعودة على TCP handshake لكنّه مدعوم بشكل سيّئ، خاصّة في middleboxes (proxies الشركات، بوّابات شركات الجوّال). صافٍ سلبيّ لقاعدة مستخدمينا؛ قد يكون لديك ملفّ مختلف.
نشر الـ API عبر hostnames كثيرة يعني عمليّات DNS كثيرة، TLS handshakes كثيرة، اتّصالات TCP كثيرة. وحّدنا إلى hostname واحد خلف reverse proxy ووجّهنا حسب المسار. يوفّر على كلّ اتّصال بارد.
دفع الخادم له وعد على الورق لكن نادراً ما يجعل الأمور أسرع عملياً. إشارات preload للمتصفّح/العميل أسهل في الاستخدام وأفضل ضبطاً. لا نستخدم دفع الخادم.
رقمان محدّدان:
كلّ نداء API يسجّل DNS، TCP-connect، TLS، request-write، server-wait، response-read. التفصيل يهمّ: إن كان DNS أكبر قطعة، الإصلاح مختلف عن «TLS أكبر قطعة». TTFB المُجمَّع يخفي أيّ جزء للتحسين.
من كلّ طلباتنا، ما النسبة المئويّة التي تُجرى على اتّصال كان مفتوحاً أصلاً؟ يجب أن تكون > ٩٥٪ في الحالة المستقرّة. إن انخفض، شيء يقتل الاتّصالات (timeouts proxy، حدود الخادم، سوء استخدام العميل).
معظم شكاوى «التطبيق بطيء» عن إعداد الاتّصال، لا المعالجة. الإصلاحات في معظمها بنيويّة: أبقِ اتّصالاً واحداً مفتوحاً، سخّنه مسبقاً عند الإقلاع، ضع خادمك جغرافيّاً قريباً من مستخدميك، اضغط كلّ شيء. لا شيء منها glamorous؛ كلّها تحرّك الأرقام.
تصفّح موضوع الأداء لبقيّة السلسلة، أو اقفز إلى سلسلة محرّك المزامنة لأخذ مختلف على ما تفعله الشبكات السيّئة بالعمارة.