Node.js و Express
تحسين الأداء
تحسين أداء Node.js
يضمن تحسين الأداء أن يعمل تطبيق Node.js الخاص بك بكفاءة، ويتعامل مع حركة المرور العالية، ويوفر أوقات استجابة سريعة. يتضمن ذلك التحليل والتخزين المؤقت وتحسين قاعدة البيانات والإدارة الصحيحة للموارد.
تحليل تطبيقات Node.js
يساعدك التحليل في تحديد اختناقات الأداء. يوفر Node.js أدوات تحليل مدمجة:
// 1. استخدام محلل Node.js المدمج node --prof server.js // هذا ينشئ ملفًا: isolate-0x....-v8.log // معالجته للحصول على إخراج قابل للقراءة node --prof-process isolate-0x....-v8.log > processed.txt // 2. استخدام Chrome DevTools للتحليل node --inspect server.js // افتح chrome://inspect في متصفح Chrome // انقر على "inspect" لفتح DevTools // انتقل إلى علامة التبويب "Profiler" لتسجيل ملفات تعريف CPU // 3. استخدام clinic.js للتشخيص الشامل npm install -g clinic // Doctor - كشف مشاكل الأداء clinic doctor -- node server.js // Bubbleprof - تحليل العمليات غير المتزامنة clinic bubbleprof -- node server.js // Flame - تحليل CPU clinic flame -- node server.js
كشف تسرب الذاكرة
يحدث تسرب الذاكرة عندما يخصص تطبيقك الذاكرة باستمرار دون تحريرها، مما يؤدي في النهاية إلى تعطل العملية.
الأسباب الشائعة لتسرب الذاكرة:
- المتغيرات العامة التي تستمر في النمو
- مستمعي الأحداث الذين لا تتم إزالتهم أبدًا
- الإغلاقات التي تحتفظ بمراجع لكائنات كبيرة
- التخزين المؤقت بدون حدود للحجم
- المؤقتات (setTimeout/setInterval) التي لم يتم مسحها
// كشف تسرب الذاكرة
const heapdump = require('heapdump');
// أخذ لقطة من الذاكرة
app.get('/heapdump', (req, res) => {
heapdump.writeSnapshot(\`./heapdump-${Date.now()}.heapsnapshot\`, (err, filename) => {
if (err) return res.status(500).send(err);
res.send(`تم كتابة تفريغ الذاكرة إلى ${filename}`);
});
});
// سيئ: مثال على تسرب الذاكرة
const cache = {};
app.get('/cache/:key', (req, res) => {
cache[req.params.key] = req.body; // التخزين المؤقت ينمو بلا حدود!
res.send('تم التخزين المؤقت');
});
// جيد: ذاكرة تخزين مؤقت محدودة مع LRU (الأقل استخدامًا مؤخرًا)
const LRU = require('lru-cache');
const cache = new LRU({
max: 500, // بحد أقصى 500 عنصر
maxAge: 1000 * 60 * 60 // تنتهي صلاحية العناصر بعد ساعة واحدة
});
app.get('/cache/:key', (req, res) => {
cache.set(req.params.key, req.body);
res.send('تم التخزين المؤقت');
});
// سيئ: تسرب مستمع الحدث
class BadEmitter extends EventEmitter {
addListener() {
this.on('data', () => {
// معالجة البيانات
});
// لا تتم إزالة المستمع أبدًا!
}
}
// جيد: إزالة المستمعين
class GoodEmitter extends EventEmitter {
addListener() {
const handler = () => { /* معالجة البيانات */ };
this.on('data', handler);
// تنظيف عند الانتهاء
this.once('close', () => {
this.removeListener('data', handler);
});
}
}
تحليل وتحسين CPU
حدد العمليات كثيفة الاستخدام لوحدة المعالجة المركزية وقم بتحسينها:
// سيئ: حظر حلقة الحدث
app.get('/calculate', (req, res) => {
let result = 0;
for (let i = 0; i < 10000000000; i++) {
result += Math.sqrt(i);
}
res.json({ result });
});
// جيد: نقل العمل كثيف الاستخدام للمعالج إلى خيوط العمال
const { Worker } = require('worker_threads');
app.get('/calculate', (req, res) => {
const worker = new Worker('./calc-worker.js');
worker.on('message', (result) => {
res.json({ result });
});
worker.on('error', (err) => {
res.status(500).json({ error: err.message });
});
worker.postMessage({ iterations: 10000000000 });
});
// calc-worker.js
const { parentPort } = require('worker_threads');
parentPort.on('message', ({ iterations }) => {
let result = 0;
for (let i = 0; i < iterations; i++) {
result += Math.sqrt(i);
}
parentPort.postMessage(result);
});
استراتيجيات التخزين المؤقت
يقلل التخزين المؤقت من استعلامات قاعدة البيانات ويحسن أوقات الاستجابة بشكل كبير:
const Redis = require('redis');
const redisClient = Redis.createClient({
host: 'localhost',
port: 6379
});
// وسيط التخزين المؤقت
const cacheMiddleware = (duration) => {
return async (req, res, next) => {
const key = `cache:${req.originalUrl}`;
try {
const cachedData = await redisClient.get(key);
if (cachedData) {
console.log('إصابة التخزين المؤقت!');
return res.json(JSON.parse(cachedData));
}
// تعديل res.json لتخزين الاستجابة مؤقتًا
const originalJson = res.json.bind(res);
res.json = (data) => {
redisClient.setEx(key, duration, JSON.stringify(data));
return originalJson(data);
};
next();
} catch (err) {
console.error('خطأ في التخزين المؤقت:', err);
next();
}
};
};
// استخدام التخزين المؤقت
app.get('/api/products', cacheMiddleware(3600), async (req, res) => {
const products = await db.query('SELECT * FROM products');
res.json(products);
});
// إبطال التخزين المؤقت
app.post('/api/products', async (req, res) => {
const product = await db.insert(req.body);
// إبطال ذاكرة التخزين المؤقت ذات الصلة
await redisClient.del('cache:/api/products');
res.json(product);
});
وسيط الضغط
ضغط الاستجابات لتقليل النطاق الترددي وتحسين أوقات التحميل:
const compression = require('compression');
// تمكين الضغط
app.use(compression({
level: 6, // مستوى الضغط (0-9)
threshold: 1024, // ضغط الاستجابات فقط > 1KB
filter: (req, res) => {
// لا تضغط إذا كان العميل لا يدعمه
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
}
}));
// يقلل الضغط من حجم الاستجابة بنسبة 60-80٪ لمحتوى النص!
تحسين استعلامات قاعدة البيانات
تقنيات تحسين قاعدة البيانات:
- الفهرسة: إضافة فهارس على الأعمدة المستعلم عنها بشكل متكرر
- تحليل الاستعلام: استخدام EXPLAIN لتحليل أداء الاستعلام
- تجنب استعلامات N+1: استخدام JOINs أو التحميل الدفعي
- تحديد مجموعات النتائج: دائمًا صفحات البيانات الكبيرة
- البيانات المحضرة: تنفيذ أسرع ومنع حقن SQL
// سيئ: مشكلة استعلام N+1
const users = await db.query('SELECT * FROM users');
for (const user of users) {
user.posts = await db.query('SELECT * FROM posts WHERE user_id = ?', [user.id]);
}
// جيد: استعلام واحد مع JOIN
const usersWithPosts = await db.query(`
SELECT
users.*,
JSON_ARRAYAGG(
JSON_OBJECT('id', posts.id, 'title', posts.title)
) as posts
FROM users
LEFT JOIN posts ON posts.user_id = users.id
GROUP BY users.id
`);
// استخدام الترقيم لمجموعات البيانات الكبيرة
app.get('/api/posts', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = 20;
const offset = (page - 1) * limit;
const posts = await db.query(
'SELECT * FROM posts ORDER BY created_at DESC LIMIT ? OFFSET ?',
[limit, offset]
);
const total = await db.query('SELECT COUNT(*) as count FROM posts');
res.json({
posts,
pagination: {
page,
limit,
total: total[0].count,
pages: Math.ceil(total[0].count / limit)
}
});
});
تجميع الاتصالات
إعادة استخدام اتصالات قاعدة البيانات بدلاً من إنشاء اتصالات جديدة لكل طلب:
const mysql = require('mysql2/promise');
// إنشاء تجمع الاتصال
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'myapp',
waitForConnections: true,
connectionLimit: 10, // بحد أقصى 10 اتصالات متزامنة
queueLimit: 0, // قائمة انتظار غير محدودة
enableKeepAlive: true,
keepAliveInitialDelay: 0
});
// استخدام التجمع للاستعلامات
app.get('/api/users', async (req, res) => {
try {
const [rows] = await pool.execute('SELECT * FROM users');
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'خطأ في قاعدة البيانات' });
}
});
// مراقبة حالة التجمع
setInterval(() => {
console.log('اتصالات التجمع:', pool.pool._allConnections.length);
console.log('الاتصالات النشطة:', pool.pool._acquiringConnections.length);
}, 30000);
التحميل الكسول وتقسيم الكود
// سيئ: تحميل كل شيء مقدمًا
const heavyModule = require('./heavy-module');
app.get('/heavy-operation', (req, res) => {
const result = heavyModule.process(req.body);
res.json(result);
});
// جيد: التحميل الكسول للوحدات عند الحاجة
app.get('/heavy-operation', async (req, res) => {
// فقط تطلب الوحدة عند الوصول إلى هذا المسار
const heavyModule = require('./heavy-module');
const result = await heavyModule.process(req.body);
res.json(result);
});
// أفضل: استخدام الواردات الديناميكية للتحميل غير المتزامن
app.get('/heavy-operation', async (req, res) => {
const { default: heavyModule } = await import('./heavy-module.mjs');
const result = await heavyModule.process(req.body);
res.json(result);
});
مراقبة مقاييس الأداء
const promClient = require('prom-client');
// إنشاء سجل
const register = new promClient.Registry();
// إضافة المقاييس الافتراضية
promClient.collectDefaultMetrics({ register });
// مقاييس مخصصة
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'مدة طلبات HTTP بالثواني',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.5, 1, 2, 5]
});
register.registerMetric(httpRequestDuration);
// قياس مدة الطلب
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
httpRequestDuration
.labels(req.method, req.route?.path || req.path, res.statusCode)
.observe(duration);
});
next();
});
// نقطة نهاية المقاييس
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
تمرين عملي
المهمة: تحسين تطبيق Node.js بطيء:
- قم بتحليل تطبيق موجود لتحديد الاختناقات
- قم بتنفيذ التخزين المؤقت Redis للبيانات التي يتم الوصول إليها بشكل متكرر
- قم بتحسين استعلامات قاعدة البيانات (إزالة استعلامات N+1)
- أضف تجميع الاتصال لقاعدة البيانات
- قم بتنفيذ وسيط الضغط
- انقل العمليات كثيفة الاستخدام لوحدة المعالجة المركزية إلى خيوط العمال
- قم بإعداد مراقبة الأداء بمقاييس Prometheus
- أنشئ نقطة نهاية كشف تسرب الذاكرة
- قارن الأداء قبل/بعد باستخدام اختبار التحميل (Artillery، k6)
قائمة التحقق من تحسين الأداء:
- ✓ استخدام تجميع الاتصال لقواعد البيانات
- ✓ تنفيذ التخزين المؤقت (Redis، في الذاكرة)
- ✓ تمكين ضغط gzip
- ✓ تحسين استعلامات قاعدة البيانات وإضافة فهارس
- ✓ استخدام CDN للأصول الثابتة
- ✓ تنفيذ التحميل الكسول للوحدات
- ✓ مراقبة استخدام الذاكرة وكشف التسريبات
- ✓ استخدام خيوط العمال للمهام كثيفة الاستخدام لوحدة المعالجة المركزية
- ✓ إعداد التسجيل والمراقبة المناسبة