Node.js Performance Optimization: A Practical Guide
Node.js Performance Optimization: A Practical Guide
Building fast Node.js applications requires understanding the event loop, memory management, and optimization techniques. This guide covers practical strategies to improve your Node.js performance.
Understanding the Event Loop
Node.js is single-threaded but highly scalable due to its event-driven architecture.
Event Loop Phases
// The event loop has several phases:
// 1. Timers - setTimeout, setInterval
// 2. Pending callbacks
// 3. Idle, prepare
// 4. Poll - I/O callbacks
// 5. Check - setImmediate
// 6. Close callbacks
// Blocking the event loop (BAD)
app.get('/compute', (req, res) => {
const result = heavyComputation(); // Blocks event loop
res.json(result);
});
// Non-blocking approach (GOOD)
app.get('/compute', async (req, res) => {
const result = await new Promise(resolve => {
setImmediate(() => resolve(heavyComputation()));
});
res.json(result);
});
Async Patterns for Performance
Using Worker Threads
// worker.js
const { parentPort } = require('worker_threads');
parentPort.on('message', (data) => {
const result = heavyComputation(data);
parentPort.postMessage(result);
});
// main.js
const { Worker } = require('worker_threads');
function runWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js');
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker stopped with code ${code}`));
});
worker.postMessage(data);
});
}
Cluster Mode
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
const cpuCount = os.cpus().length;
for (let i = 0; i < cpuCount; i++) {
cluster.fork();
}
cluster.on('exit', (worker) => {
console.log(`Worker ${worker.id} died. Restarting...`);
cluster.fork();
});
} else {
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send(`Handled by worker ${process.pid}`);
});
app.listen(3000);
}
Memory Management
Avoiding Memory Leaks
// BAD: Global accumulation
const cache = {};
app.get('/data/:id', (req, res) => {
cache[req.params.id] = largeObject; // Memory leak!
res.json(cache[req.params.id]);
});
// GOOD: LRU Cache
const LRU = require('lru-cache');
const cache = new LRU({ max: 500, maxAge: 1000 * 60 * 60 });
app.get('/data/:id', (req, res) => {
const cached = cache.get(req.params.id);
if (cached) return res.json(cached);
const data = fetchData(req.params.id);
cache.set(req.params.id, data);
res.json(data);
});
Event Listener Cleanup
// BAD: Leaking listeners
class DataEmitter extends EventEmitter {
constructor() {
super();
setInterval(() => this.emit('data', getData()), 1000);
}
}
// GOOD: Cleanup on destroy
class DataEmitter extends EventEmitter {
constructor() {
super();
this.interval = setInterval(() => this.emit('data', getData()), 1000);
}
destroy() {
clearInterval(this.interval);
this.removeAllListeners();
}
}
Streaming for Large Data
// BAD: Load entire file into memory
app.get('/download', async (req, res) => {
const data = await fs.readFile('large-file.csv');
res.send(data);
});
// GOOD: Stream the file
app.get('/download', (req, res) => {
const stream = fs.createReadStream('large-file.csv');
stream.pipe(res);
});
// Transform streams for processing
const { Transform } = require('stream');
const csvToJson = new Transform({
objectMode: true,
transform(chunk, encoding, callback) {
const json = csvRowToJson(chunk.toString());
callback(null, JSON.stringify(json) + '\n');
}
});
fs.createReadStream('data.csv')
.pipe(csvToJson)
.pipe(fs.createWriteStream('data.jsonl'));
Database Optimization
Connection Pooling
const { Pool } = require('pg');
const pool = new Pool({
max: 20, // Maximum connections
min: 5, // Minimum connections
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
app.get('/users/:id', async (req, res) => {
const result = await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
res.json(result.rows[0]);
});
Query Optimization
// BAD: N+1 queries
app.get('/posts', async (req, res) => {
const posts = await db.query('SELECT * FROM posts');
for (const post of posts) {
post.author = await db.query('SELECT * FROM users WHERE id = ?', [post.author_id]);
}
res.json(posts);
});
// GOOD: Single query with JOIN
app.get('/posts', async (req, res) => {
const posts = await db.query(`
SELECT posts.*, users.name as author_name
FROM posts
JOIN users ON posts.author_id = users.id
`);
res.json(posts);
});
Caching Strategies
In-Memory Caching
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600, checkperiod: 120 });
async function getWithCache(key, fetchFn) {
const cached = cache.get(key);
if (cached) return cached;
const data = await fetchFn();
cache.set(key, data);
return data;
}
app.get('/products', async (req, res) => {
const products = await getWithCache('products', () =>
db.query('SELECT * FROM products')
);
res.json(products);
});
Redis Caching
const Redis = require('ioredis');
const redis = new Redis();
async function getWithRedisCache(key, fetchFn, ttl = 3600) {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const data = await fetchFn();
await redis.setex(key, ttl, JSON.stringify(data));
return data;
}
Profiling and Monitoring
Built-in Profiler
# Start with profiler
node --prof app.js
# Generate report
node --prof-process isolate-*.log > profile.txt
Clinic.js
# Install clinic
npm install -g clinic
# Analyze event loop delays
clinic doctor -- node app.js
# Analyze memory
clinic heapprofiler -- node app.js
# Flame graph
clinic flame -- node app.js
Custom Monitoring
// Monitor event loop lag
let lastCheck = Date.now();
setInterval(() => {
const now = Date.now();
const lag = now - lastCheck - 1000;
console.log(`Event loop lag: ${lag}ms`);
lastCheck = now;
}, 1000);
// Monitor memory
setInterval(() => {
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`,
});
}, 5000);
Middleware Optimization
// BAD: Synchronous middleware
app.use((req, res, next) => {
req.user = verifyTokenSync(req.headers.authorization);
next();
});
// GOOD: Async middleware
app.use(async (req, res, next) => {
try {
req.user = await verifyToken(req.headers.authorization);
next();
} catch (err) {
next(err);
}
});
// Conditional middleware
const conditionalMiddleware = (condition, middleware) => {
return (req, res, next) => {
if (condition(req)) {
middleware(req, res, next);
} else {
next();
}
};
};
Performance Checklist
- Use async/await instead of blocking operations
- Implement connection pooling for databases
- Use streams for large data processing
- Cache frequently accessed data
- Monitor memory usage and event loop lag
- Use cluster mode for CPU-intensive tasks
- Profile regularly with clinic or --prof
- Compress responses with middleware
- Use CDN for static assets
- Implement rate limiting to prevent abuse
// Essential middleware
const compression = require('compression');
const rateLimit = require('express-rate-limit');
app.use(compression());
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
Conclusion
Optimizing Node.js performance requires understanding the event loop, proper memory management, and implementing caching strategies. Regular profiling and monitoring help identify bottlenecks before they become problems. Apply these techniques incrementally and measure their impact.