الأداء
لماذا يبطؤ موقع ووردبريس، وكيف تشخّص السبب فعلياً
زمن أول بايت يفصل مشكلة الخادم عن مشكلة المتصفح. من هذا الرقم تبدأ، ثم تلاحق الاستعلامات وجدول الخيارات قبل أن تفكّر في ذاكرة مؤقتة.
endpoint يستغرق 900 مللي ثانية ليس لغزاً، لكنه غير مقروء من الخارج. قبل أن تغيّر شيئاً، اجعل الطلب نفسه يخبرك أين ذهب وقته. فعّل query logging في الـ ORM وعُدّ الجُمَل في طلب واحد:
// TypeORM
new DataSource({logging: ['query'], maxQueryExecutionTime: 100});رقمان يحسمان معظم الحالات: كم query نفّذ الطلب الواحد، وكم استغرقت أبطأ واحدة فيه؟ query واحدة بـ 400 مللي ثانية مشكلة index، و60 query بـ 4 مللي ثانية لكلٍّ منها مشكلة بنية — وهي الأكثر شيوعاً بفارق كبير.
تجلب 50 order، ثم تقرأ order.customer داخل حلقة، فيصدر الـ ORM 50 query إضافية دون أن يخبرك. يبدو الأمر كقراءة property، وهو في الحقيقة round trips على الشبكة.
// query لكل order، بصمت
const orders = await repo.find();
for (const o of orders) console.log(o.customer.name);
// query واحدة، بـ join
const orders = await repo.find({relations: {customer: true}});الـ lazy relations هي السبب المعتاد، وسبب ضررها أنها تجعل الشيء المكلف يبدو مجانياً. وإن كنت تستخدم GraphQL، فالمشكلة نفسها تصل لكل field عبر الـ resolvers، وعلاجها DataLoader يجمّع الطلبات ضمن الدورة الواحدة، لا الـ eager relations.
اقرأ عدد الـ queries في أبطأ 3 endpoints عندك. إن كان يتناسب مع عدد الصفوف المُعادة، فقد وجدت العطب، وكل ما تحته ثانوي.
كل طلب يحجز connection بقاعدة البيانات طوال عمله. وللـ pool حجم، 10 تقريباً في العادة، وحين تنشغل كلها ينتظر الطلب التالي — لا يبطؤ، بل ينتظر، فيكون ما تقيسه طابوراً لا عملاً.
ورفع حجم الـ pool خطوة أولى خاطئة، إذ ينقل التزاحم إلى قاعدة البيانات غالباً. قصّر بدل ذلك المدة التي يحجز فيها كل طلب connection: لا استدعاءات HTTP داخل transaction، ولا await على شيء غير ذي صلة بين الفتح والـ commit، ولا transaction تمتد عبر طلب لا تتحكم به.
interceptor والـ pipe العامة تعمل على كل route. فالـ ValidationPipe مع transform على payload كبير، أو interceptor تسجيل يُسلسِل الـ response body كاملاً، كلفة تدفعها في كل طلب ولا تراها في query log أبداً.class-transformer ليست مجانية. استدعاء plainToInstance على بضعة آلاف صف استهلاك CPU حقيقي. أعِد الشكل الذي تحتاجه من الـ query بدل جلب الـ entities ثم تحويلها.request-scoped providers تعيد بناء شجرة الـ DI في كل طلب. مريحة، وأغلى بكثير من نطاق singleton الافتراضي. استخدمها عن قصد لا عن عادة.await داخل حلقة تسلسل. استخدام Promise.all على أعمال مستقلة تغيير من سطر واحد بأثر كبير في العادة.الـ cache خطوة تأتي بعد أن تصحّ البنية، لأن cache أمام مشكلة N+1 يخفيها، وستعود على شكل stampede أول مرة تنتهي صلاحيته تحت حِمل.
وحين تضيفه، خزّن الـ query المكلفة لا الـ response كاملة، وأعطه TTL صريحاً، وقرّر ما الذي يُبطله قبل أن تنشره. cache بلا حكاية invalidation عطبٌ مؤجَّل.
بعد كل تغيير، قِس الـ endpoint نفسه بالطريقة نفسها — بتزامن تتوقعه فعلاً، وبالمئين 95 (p95) لا بالمتوسط. المتوسطات تخفي الطلبات التي تخسر بها عملاءك، وسبب إصلاح N+1 أولاً أنها هي التي تسوء بشدة في اللحظة التي تكون فيها أكثر انشغالاً.
الأداء
زمن أول بايت يفصل مشكلة الخادم عن مشكلة المتصفح. من هذا الرقم تبدأ، ثم تلاحق الاستعلامات وجدول الخيارات قبل أن تفكّر في ذاكرة مؤقتة.
الأمن
4 ثغرات في NestJS خلال 7 أشهر، وكلها ثغرة واحدة: الـ matcher والـ handler اختلفا على العنوان نفسه. مكان التحقق هو حيث تُقرأ البيانات.
SEO
يدمج Next.js الـ metadata مفتاحاً مفتاحاً، فالـ route الذي يغفل alternates يرث canonical الخاص بالـ layout. وعندنا كان يشير بكل صفحة إلى الرئيسية.