تحسين الأداء في Node.js
مقدمة إلى أداء Node.js
تحسين الأداء أمر بالغ الأهمية لبناء تطبيقات Node.js قابلة للتوسع. فهم كيفية عمل حلقة الأحداث، وتحديد الاختناقات، وتطبيق تقنيات التحسين يمكن أن يحسن بشكل كبير من إنتاجية تطبيقك وأوقات الاستجابة.
المبدأ الأساسي: Node.js أحادي الخيط مع I/O غير محجوب. المهام كثيفة الاستخدام لوحدة المعالجة المركزية تحجب حلقة الأحداث، بينما يتم التعامل مع عمليات I/O بشكل غير متزامن. قم بالتحسين وفقًا لذلك.
فهم حلقة الأحداث
حلقة الأحداث هي قلب Node.js. فهم مراحلها يساعد في تحديد مشاكل الأداء:
// مراحل حلقة الأحداث:
// 1. المؤقتات (setTimeout, setInterval)
// 2. الاستدعاءات المعلقة (استدعاءات I/O المؤجلة للحلقة التالية)
// 3. الخمول، الإعداد (الاستخدام الداخلي)
// 4. الاستطلاع (استرجاع أحداث I/O الجديدة)
// 5. الفحص (استدعاءات setImmediate)
// 6. إغلاق الاستدعاءات (socket.on('close'))
// تصور حلقة الأحداث
const eventLoopMonitor = () => {
const start = Date.now();
let iterations = 0;
const check = () => {
iterations++;
const elapsed = Date.now() - start;
if (elapsed < 1000) {
setImmediate(check);
} else {
console.log(`حلقة الأحداث: ${iterations} تكرار/ثانية`);
}
};
check();
};
// قياس تأخر حلقة الأحداث
const eventLoopLag = require('event-loop-lag');
const lag = eventLoopLag(1000); // فحص كل ثانية
setInterval(() => {
console.log(`تأخر حلقة الأحداث: ${lag()}ms`);
}, 5000);
// محجوب مقابل غير محجوب
// سيء - يحجب حلقة الأحداث
function calculateSum(n) {
let sum = 0;
for (let i = 0; i < n; i++) {
sum += i;
}
return sum;
}
// جيد - استخدام خيوط العمل للمهام كثيفة الاستخدام لوحدة المعالجة المركزية
const { Worker } = require('worker_threads');
function calculateSumAsync(n) {
return new Promise((resolve, reject) => {
const worker = new Worker(`
const { parentPort, workerData } = require('worker_threads');
let sum = 0;
for (let i = 0; i < workerData.n; i++) {
sum += i;
}
parentPort.postMessage(sum);
`, { eval: true, workerData: { n } });
worker.on('message', resolve);
worker.on('error', reject);
});
}
تحليل تطبيقات Node.js
استخدم أدوات التحليل المدمجة لتحديد اختناقات الأداء:
// 1. محلل Node.js المدمج
// البدء مع علامة --prof
node --prof app.js
// إنشاء تقرير قابل للقراءة
node --prof-process isolate-0xnnnnnnnnnnnn-v8.log > processed.txt
// 2. Chrome DevTools
// البدء مع علامة --inspect
node --inspect app.js
// فتح chrome://inspect في Chrome
// 3. clinic.js - تحليل شامل
npm install -g clinic
// Doctor - كشف مشاكل حلقة الأحداث
clinic doctor -- node app.js
// Bubbleprof - العمليات غير المتزامنة
clinic bubbleprof -- node app.js
// Flame - تحليل CPU
clinic flame -- node app.js
// 4. مراقبة الأداء المخصصة
const { performance, PerformanceObserver } = require('perf_hooks');
// قياس وقت تنفيذ الدالة
performance.mark('start-operation');
await someExpensiveOperation();
performance.mark('end-operation');
performance.measure('operation', 'start-operation', 'end-operation');
const obs = new PerformanceObserver((items) => {
items.getEntries().forEach((entry) => {
console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
});
});
obs.observe({ entryTypes: ['measure'] });
كشف تسرب الذاكرة
تحدث تسربات الذاكرة عندما لا يتم تحرير الذاكرة غير المستخدمة. اكتشفها وأصلحها:
// 1. مراقبة استخدام الذاكرة
const used = process.memoryUsage();
console.log({
rss: `${Math.round(used.rss / 1024 / 1024)}MB`, // حجم المجموعة المقيمة
heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)}MB`,
heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)}MB`,
external: `${Math.round(used.external / 1024 / 1024)}MB`
});
// 2. لقطات الكومة لكشف التسربات
const v8 = require('v8');
const fs = require('fs');
function takeHeapSnapshot() {
const filename = `heap-${Date.now()}.heapsnapshot`;
const heapSnapshot = v8.writeHeapSnapshot(filename);
console.log(`تم كتابة لقطة الكومة إلى ${heapSnapshot}`);
}
// أخذ لقطات على فترات ومقارنتها
setInterval(takeHeapSnapshot, 60000);
// 3. أنماط تسرب الذاكرة الشائعة
// سيء - تراكم المتغيرات العامة
let cache = [];
function addToCache(data) {
cache.push(data); // لا يتم مسحه أبدًا!
}
// جيد - استخدام ذاكرة تخزين مؤقت صحيحة مع حدود الحجم
const LRU = require('lru-cache');
const cache = new LRU({
max: 500,
maxAge: 1000 * 60 * 60 // ساعة واحدة
});
// سيء - تسرب مستمعي الأحداث
function setupListeners() {
const emitter = new EventEmitter();
setInterval(() => {
emitter.on('data', handleData); // مستمع جديد كل ثانية!
}, 1000);
}
// جيد - إزالة المستمعين أو استخدام once()
function setupListeners() {
const emitter = new EventEmitter();
emitter.once('data', handleData); // تتم إزالته تلقائيًا
// أو: emitter.removeListener('data', handleData);
}
// سيء - الإغلاق يحتفظ بالكائنات الكبيرة
function createClosure() {
const largeObject = new Array(1000000);
return function() {
console.log(largeObject.length); // يحتفظ بـ largeObject في الذاكرة
};
}
// جيد - التقط فقط ما تحتاجه
function createClosure() {
const largeObject = new Array(1000000);
const length = largeObject.length;
return function() {
console.log(length); // يخزن الرقم فقط
};
}
تحذير من تسرب الذاكرة: مستمعو الأحداث، والذاكرة المؤقتة العامة، والإغلاقات هي مصادر شائعة لتسرب الذاكرة. قم دائمًا بتنظيف الموارد واستخدم المراجع الضعيفة عند الاقتضاء.
تحسين استعلامات قاعدة البيانات
استعلامات قاعدة البيانات غالبًا ما تكون أكبر اختناق في الأداء:
// 1. استخدام الفهارس
// إنشاء فهارس على الحقول المستعلمة بشكل متكرر
await db.schema.table('users', (table) => {
table.index('email');
table.index('created_at');
table.index(['status', 'created_at']); // فهرس مركب
});
// 2. تحديد الأعمدة المطلوبة فقط
// سيء
const users = await db('users').select('*');
// جيد
const users = await db('users').select('id', 'name', 'email');
// 3. استخدام الترقيم
// سيء - تحميل جميع السجلات
const allUsers = await db('users').select();
// جيد - ترقيم النتائج
const page = 1;
const perPage = 20;
const users = await db('users')
.select()
.limit(perPage)
.offset((page - 1) * perPage);
// 4. تجنب استعلامات N+1
// سيء - مشكلة استعلام N+1
const posts = await db('posts').select();
for (const post of posts) {
post.author = await db('users').where({ id: post.user_id }).first();
}
// جيد - استخدام JOIN أو التحميل الحريص
const posts = await db('posts')
.select('posts.*', 'users.name as author_name')
.join('users', 'posts.user_id', 'users.id');
// 5. استخدام تجميع الاتصال
const pool = new Pool({
host: 'localhost',
database: 'mydb',
max: 20, // الحد الأقصى للاتصالات
min: 5, // الحد الأدنى للاتصالات
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000
});
// 6. تنفيذ التخزين المؤقت لنتائج الاستعلام
const cache = new Map();
async function getCachedUser(userId) {
const cacheKey = `user:${userId}`;
// فحص التخزين المؤقت
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
// استعلام قاعدة البيانات
const user = await db('users').where({ id: userId }).first();
// تخزين النتيجة مؤقتًا
cache.set(cacheKey, user);
// انتهاء الصلاحية بعد 5 دقائق
setTimeout(() => cache.delete(cacheKey), 300000);
return user;
}
استراتيجيات التخزين المؤقت
تنفيذ التخزين المؤقت متعدد الطبقات للأداء الأمثل:
// 1. التخزين المؤقت في الذاكرة
const NodeCache = require('node-cache');
const cache = new NodeCache({
stdTTL: 600, // TTL الافتراضي: 10 دقائق
checkperiod: 120, // فحص المفاتيح منتهية الصلاحية كل دقيقتين
useClones: false // عدم استنساخ الكائنات (أسرع لكن قابل للتغيير)
});
// تعيين التخزين المؤقت
cache.set('key', 'value', 300); // 5 دقائق
// الحصول على التخزين المؤقت
const value = cache.get('key');
// حذف التخزين المؤقت
cache.del('key');
// 2. التخزين المؤقت في Redis
const redis = require('redis');
const client = redis.createClient();
await client.connect();
// تعيين مع انتهاء الصلاحية
await client.set('user:123', JSON.stringify(userData), {
EX: 3600 // ساعة واحدة
});
// الحصول
const cached = await client.get('user:123');
const userData = cached ? JSON.parse(cached) : null;
// 3. التخزين المؤقت متعدد الطبقات
class CacheManager {
constructor() {
this.memCache = new NodeCache({ stdTTL: 300 });
this.redisClient = redis.createClient();
}
async get(key) {
// المستوى 1: التخزين المؤقت في الذاكرة (الأسرع)
let value = this.memCache.get(key);
if (value) {
console.log('إصابة التخزين المؤقت في الذاكرة');
return value;
}
// المستوى 2: التخزين المؤقت في Redis (سريع)
value = await this.redisClient.get(key);
if (value) {
console.log('إصابة التخزين المؤقت في Redis');
value = JSON.parse(value);
this.memCache.set(key, value); // ملء التخزين المؤقت في الذاكرة
return value;
}
console.log('فشل التخزين المؤقت');
return null;
}
async set(key, value, ttl = 3600) {
// التخزين في كلا الطبقتين
this.memCache.set(key, value, Math.min(ttl, 300)); // الحد الأقصى 5 دقائق في الذاكرة
await this.redisClient.set(key, JSON.stringify(value), { EX: ttl });
}
async delete(key) {
this.memCache.del(key);
await this.redisClient.del(key);
}
}
// 4. التخزين المؤقت HTTP مع ETags
const crypto = require('crypto');
app.get('/api/data', async (req, res) => {
const data = await fetchData();
const etag = crypto.createHash('md5').update(JSON.stringify(data)).digest('hex');
// فحص ما إذا كان العميل لديه نسخة مخزنة مؤقتًا
if (req.headers['if-none-match'] === etag) {
return res.status(304).end(); // لم يتم التعديل
}
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'public, max-age=300'); // 5 دقائق
res.json(data);
});
نصيحة احترافية: استخدم تسخين الذاكرة المؤقتة - قم بتحميل البيانات التي يتم الوصول إليها بشكل متكرر مسبقًا في الذاكرة المؤقتة أثناء بدء التشغيل لتجنب سيناريوهات الذاكرة المؤقتة الباردة.
الضغط
ضغط الاستجابات لتقليل النطاق الترددي وتحسين أوقات التحميل:
// ضغط HTTP
const compression = require('compression');
app.use(compression({
level: 6, // مستوى الضغط (0-9)
threshold: 1024, // الحد الأدنى للحجم للضغط (بايت)
filter: (req, res) => {
// عدم الضغط إذا كان العميل لا يقبل الترميز
if (req.headers['x-no-compression']) {
return false;
}
// استخدام مرشح الضغط الافتراضي
return compression.filter(req, res);
}
}));
// ضغط التدفق للملفات الكبيرة
const zlib = require('zlib');
const fs = require('fs');
app.get('/download/large-file', (req, res) => {
res.setHeader('Content-Encoding', 'gzip');
res.setHeader('Content-Type', 'application/json');
fs.createReadStream('large-file.json')
.pipe(zlib.createGzip())
.pipe(res);
});
تحسين العمليات غير المتزامنة
تحسين العمليات غير المتزامنة لأداء أفضل:
// 1. التنفيذ المتوازي مقابل التسلسلي
// سيء - تسلسلي (بطيء)
async function fetchAllData() {
const users = await fetchUsers();
const posts = await fetchPosts();
const comments = await fetchComments();
return { users, posts, comments };
}
// جيد - متوازي (سريع)
async function fetchAllData() {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
return { users, posts, comments };
}
// 2. تحديد العمليات المتزامنة
const pLimit = require('p-limit');
const limit = pLimit(5); // الحد الأقصى 5 عمليات متزامنة
const userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const users = await Promise.all(
userIds.map(id => limit(() => fetchUser(id)))
);
// 3. معالجة التدفق لمجموعات البيانات الكبيرة
const { Transform } = require('stream');
// معالجة ملف كبير دون تحميله في الذاكرة
const processStream = new Transform({
objectMode: true,
transform(chunk, encoding, callback) {
// معالجة القطعة
const processed = processData(chunk);
callback(null, processed);
}
});
fs.createReadStream('large-file.json')
.pipe(JSONStream.parse('*'))
.pipe(processStream)
.pipe(fs.createWriteStream('output.json'));
// 4. التكرار غير المتزامن للمجموعات الكبيرة
async function* fetchUsersInBatches(batchSize = 100) {
let offset = 0;
let hasMore = true;
while (hasMore) {
const users = await db('users')
.limit(batchSize)
.offset(offset);
if (users.length === 0) {
hasMore = false;
} else {
yield users;
offset += batchSize;
}
}
}
// المعالجة في دفعات
for await (const batch of fetchUsersInBatches()) {
await processBatch(batch);
}
وضع المجموعة لاستخدام متعدد النوى
استخدم وضع المجموعة للاستفادة من وحدات المعالجة المركزية متعددة النوى:
// cluster.js
const cluster = require('cluster');
const os = require('os');
const numCPUs = os.cpus().length;
if (cluster.isMaster) {
console.log(`العملية الرئيسية ${process.pid} قيد التشغيل`);
// تفريع العمال
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// إعادة تشغيل العامل إذا مات
cluster.on('exit', (worker, code, signal) => {
console.log(`العامل ${worker.process.pid} مات`);
console.log('بدء عامل جديد');
cluster.fork();
});
// إعادة تشغيل أنيقة
process.on('SIGUSR2', () => {
const workers = Object.values(cluster.workers);
const restartWorker = (workerIndex) => {
const worker = workers[workerIndex];
if (!worker) return;
worker.on('exit', () => {
if (!worker.exitedAfterDisconnect) return;
console.log(`العامل ${worker.process.pid} خرج`);
cluster.fork().on('listening', () => {
restartWorker(workerIndex + 1);
});
});
worker.disconnect();
};
restartWorker(0);
});
} else {
// عمليات العمال
require('./app');
console.log(`العامل ${process.pid} بدأ`);
}
// استخدم PM2 بدلاً من ذلك (موصى به)
// ecosystem.config.js
module.exports = {
apps: [{
name: 'my-app',
script: './app.js',
instances: 'max', // استخدام جميع نوى CPU
exec_mode: 'cluster'
}]
};
تحسين عمليات JSON
تحليل/تحويل JSON يمكن أن يكون مكلفًا للكائنات الكبيرة:
// 1. استخدام fast-json-stringify (تسلسل أسرع)
const fastJson = require('fast-json-stringify');
const stringify = fastJson({
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
email: { type: 'string' }
}
});
const json = stringify({ id: 1, name: 'أحمد', email: 'ahmad@example.com' });
// 2. تدفق JSON الكبير
const JSONStream = require('JSONStream');
app.get('/api/large-dataset', (req, res) => {
res.setHeader('Content-Type', 'application/json');
db('users')
.stream()
.pipe(JSONStream.stringify())
.pipe(res);
});
// 3. تجنب JSON.stringify المتكرر
// سيء
app.use((req, res, next) => {
console.log(JSON.stringify(req.body));
logger.info(JSON.stringify(req.body));
cache.set('body', JSON.stringify(req.body));
next();
});
// جيد
app.use((req, res, next) => {
const bodyStr = JSON.stringify(req.body);
console.log(bodyStr);
logger.info(bodyStr);
cache.set('body', bodyStr);
next();
});
تمرين: تدقيق وتحسين الأداء
خذ تطبيق Node.js موجود وقم بإجراء تدقيق أداء كامل:
- تحليل التطبيق باستخدام clinic.js وتحديد الاختناقات
- كشف وإصلاح أي تسربات في الذاكرة باستخدام لقطات الكومة
- تحسين استعلامات قاعدة البيانات (إضافة فهارس، إصلاح استعلامات N+1)
- تنفيذ التخزين المؤقت متعدد الطبقات (الذاكرة + Redis)
- إضافة وسيط الضغط
- تحويل العمليات التسلسلية إلى متوازية حيثما كان ذلك ممكنًا
- إعداد وضع المجموعة أو PM2
- قياس الأداء قبل/بعد (أوقات الاستجابة، الإنتاجية، استخدام الذاكرة)
وثق نتائجك وتحسيناتك بالمقاييس.