واتسابدمشقالسبت – الخميس·10:00 ص – 7:00 م

لماذا يبطئ الـ NestJS API عندك

رقمان يفسّران معظم الأمر: كم query نفّذ الطلب الواحد، وكم استغرق أبطؤها. 60 query صغيرة مشكلة بنية لا مشكلة index.

الأداءنُشر في 3 دقائق قراءة

سجّل الـ queries قبل أن تخمّن

endpoint يستغرق 900 مللي ثانية ليس لغزاً، لكنه غير مقروء من الخارج. قبل أن تغيّر شيئاً، اجعل الطلب نفسه يخبرك أين ذهب وقته. فعّل query logging في الـ ORM وعُدّ الجُمَل في طلب واحد:

ts
// TypeORM
new DataSource({logging: ['query'], maxQueryExecutionTime: 100});

رقمان يحسمان معظم الحالات: كم query نفّذ الطلب الواحد، وكم استغرقت أبطأ واحدة فيه؟ query واحدة بـ 400 مللي ثانية مشكلة index، و60 query بـ 4 مللي ثانية لكلٍّ منها مشكلة بنية — وهي الأكثر شيوعاً بفارق كبير.

مشكلة N+1، وهي معظم الأمر

تجلب 50 order، ثم تقرأ order.customer داخل حلقة، فيصدر الـ ORM 50 query إضافية دون أن يخبرك. يبدو الأمر كقراءة property، وهو في الحقيقة round trips على الشبكة.

ts
// 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، الخفيّ حتى لا يعود كذلك

كل طلب يحجز connection بقاعدة البيانات طوال عمله. وللـ pool حجم، 10 تقريباً في العادة، وحين تنشغل كلها ينتظر الطلب التالي — لا يبطؤ، بل ينتظر، فيكون ما تقيسه طابوراً لا عملاً.

ورفع حجم الـ pool خطوة أولى خاطئة، إذ ينقل التزاحم إلى قاعدة البيانات غالباً. قصّر بدل ذلك المدة التي يحجز فيها كل طلب connection: لا استدعاءات HTTP داخل transaction، ولا await على شيء غير ذي صلة بين الفتح والـ commit، ولا transaction تمتد عبر طلب لا تتحكم به.

أين يختبئ الوقت في Nest تحديداً

  • الـ 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 خطوة تأتي بعد أن تصحّ البنية، لأن cache أمام مشكلة N+1 يخفيها، وستعود على شكل stampede أول مرة تنتهي صلاحيته تحت حِمل.

وحين تضيفه، خزّن الـ query المكلفة لا الـ response كاملة، وأعطه TTL صريحاً، وقرّر ما الذي يُبطله قبل أن تنشره. cache بلا حكاية invalidation عطبٌ مؤجَّل.

القياس الوحيد الذي يهمّ

بعد كل تغيير، قِس الـ endpoint نفسه بالطريقة نفسها — بتزامن تتوقعه فعلاً، وبالمئين 95 (p95) لا بالمتوسط. المتوسطات تخفي الطلبات التي تخسر بها عملاءك، وسبب إصلاح N+1 أولاً أنها هي التي تسوء بشدة في اللحظة التي تكون فيها أكثر انشغالاً.

لماذا يبطئ الـ NestJS API عندك · Qasioun Cloud