Node.js Performance: Clustering, Caching, and Profiling
이 글의 핵심
Node.js performance guide: use clustering for multi-core, Redis and in-memory caching, fix N+1 queries, profiling with --prof and clinic.js, benchmarking with autocannon, and production tips.
Introduction
Why performance matters
Reasons to optimize:
- ✅ User experience: Lower latency and faster responses
- ✅ Cost: Better utilization of server resources
- ✅ Scale: Handle more concurrent users
- ✅ SEO: Page speed affects search rankings Principles:
- Measure first: Do not guess—profile and measure
- Find bottlenecks: Improve the slowest parts first
- Iterate: Change one thing at a time
- Trade-offs: Balance performance vs readability and maintainability
1. Clustering
Cluster module
// cluster.js
const cluster = require('cluster');
const http = require('http');
const os = require('os');
const numCPUs = os.cpus().length;
if (cluster.isMaster) {
console.log(`Master process ${process.pid} running`);
// Fork worker processes
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// Restart workers on exit
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} exited`);
console.log('Starting new worker...');
cluster.fork();
});
} else {
// Workers run the HTTP server
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end(`Handled by worker ${process.pid}\n`);
});
server.listen(3000, () => {
console.log(`Worker ${process.pid} started`);
});
}
Run:
node cluster.js
# Master process 12345 running
# Worker 12346 started
# ...
PM2 cluster mode
# One process per CPU core
pm2 start app.js -i max
# Fixed count
pm2 start app.js -i 4
# Zero-downtime reload
pm2 reload app
2. Caching
In-memory cache
// Simple TTL cache
const cache = new Map();
function getCachedData(key, fetchFn, ttl = 60000) {
const cached = cache.get(key);
if (cached && Date.now() < cached.expiresAt) {
console.log('Cache hit');
return cached.data;
}
console.log('Cache miss');
const data = fetchFn();
cache.set(key, {
data,
expiresAt: Date.now() + ttl
});
return data;
}
// Usage
app.get('/api/users', async (req, res) => {
const users = getCachedData('users', async () => {
return await User.find();
}, 60000); // 1 minute
res.json({ users });
});
Redis caching
npm install redis
// cache.js
const redis = require('redis');
const client = redis.createClient({
host: 'localhost',
port: 6379
});
client.on('error', (err) => {
console.error('Redis error:', err);
});
client.connect();
async function setCache(key, value, ttl = 3600) {
await client.setEx(key, ttl, JSON.stringify(value));
}
async function getCache(key) {
const value = await client.get(key);
return value ? JSON.parse(value) : null;
}
async function deleteCache(key) {
await client.del(key);
}
module.exports = { setCache, getCache, deleteCache };
// Usage
const { getCache, setCache } = require('./cache');
app.get('/api/users/:id', async (req, res) => {
const { id } = req.params;
const cacheKey = `user:${id}`;
let user = await getCache(cacheKey);
if (user) {
console.log('Cache hit');
return res.json({ user, cached: true });
}
user = await User.findById(id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
await setCache(cacheKey, user, 3600);
res.json({ user, cached: false });
});
Cache invalidation
app.put('/api/users/:id', async (req, res) => {
const { id } = req.params;
const user = await User.findByIdAndUpdate(id, req.body, { new: true });
await deleteCache(`user:${id}`);
res.json({ user });
});
3. Database optimization
Indexes
// MongoDB
userSchema.index({ email: 1 });
userSchema.index({ name: 1, age: -1 });
userSchema.index({ email: 1 }, { unique: true });
const indexes = await User.collection.getIndexes();
console.log(indexes);
Query optimization
// Slow: N+1
const posts = await Post.find();
for (const post of posts) {
const author = await User.findById(post.author);
}
// Prefer populate
const posts = await Post.find().populate('author');
// Project only needed fields
const posts = await Post.find()
.select('title content author')
.populate('author', 'name email');
// lean(): plain objects, faster reads
const posts = await Post.find().lean();
Connection pool
mongoose.connect('mongodb://localhost:27017/mydb', {
maxPoolSize: 10,
minPoolSize: 2
});
const pool = new Pool({
max: 20,
min: 5,
idleTimeoutMillis: 30000
});
4. Async optimization
Parallelism
// Sequential (slow)
async function sequential() {
const users = await User.find();
const posts = await Post.find();
const comments = await Comment.find();
return { users, posts, comments };
}
// Total time: T1 + T2 + T3
// Parallel (fast)
async function parallel() {
const [users, posts, comments] = await Promise.all([
User.find(),
Post.find(),
Comment.find()
]);
return { users, posts, comments };
}
// Total time: max(T1, T2, T3)
Concurrency limit
const pLimit = require('p-limit');
async function processFiles(files) {
const limit = pLimit(5);
const results = await Promise.all(
files.map(file => limit(() => processFile(file)))
);
return results;
}
5. Memory management
Inspect memory
function logMemoryUsage() {
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`
});
}
setInterval(logMemoryUsage, 60000);
Avoid unbounded caches
// Bad: unbounded Map growth
const cache = new Map();
app.get('/api/data/:id', async (req, res) => {
const data = await fetchData(req.params.id);
cache.set(req.params.id, data);
res.json(data);
});
// Better: LRU
const LRU = require('lru-cache');
const cache = new LRU({
max: 500,
maxAge: 1000 * 60 * 60
});
app.get('/api/data/:id', async (req, res) => {
let data = cache.get(req.params.id);
if (!data) {
data = await fetchData(req.params.id);
cache.set(req.params.id, data);
}
res.json(data);
});
Streams for large payloads
// Bad: load entire file into memory
app.get('/download', async (req, res) => {
const data = await fs.promises.readFile('large-file.pdf');
res.send(data);
});
// Good: stream
app.get('/download', (req, res) => {
fs.createReadStream('large-file.pdf').pipe(res);
});
6. Profiling
Built-in CPU profiler
node --prof app.js
node --prof-process isolate-0x*.log > processed.txt
Chrome DevTools
node --inspect app.js
node --inspect-brk app.js
Open chrome://inspect and attach for CPU/memory profiling.
clinic.js
npm install -g clinic
clinic doctor -- node app.js
clinic bubbleprof -- node app.js
clinic heapprofiler -- node app.js
7. Benchmarking
Apache Bench
sudo apt install apache2-utils
ab -n 1000 -c 10 http://localhost:3000/
autocannon
npm install -g autocannon
autocannon -c 10 -d 10 http://localhost:3000/
Programmatic benchmark
const autocannon = require('autocannon');
async function runBenchmark() {
const result = await autocannon({
url: 'http://localhost:3000',
connections: 10,
duration: 10,
pipelining: 1
});
console.log('Req/sec:', result.requests.mean);
console.log('Latency ms:', result.latency.mean);
console.log('Throughput:', result.throughput.mean, 'bytes/sec');
}
runBenchmark();
8. Practical examples
Example 1: API response caching middleware
const express = require('express');
const redis = require('redis');
const app = express();
const client = redis.createClient();
await client.connect();
function cacheMiddleware(ttl = 3600) {
return async (req, res, next) => {
const key = `cache:${req.originalUrl}`;
try {
const cached = await client.get(key);
if (cached) {
console.log('Cache hit');
return res.json(JSON.parse(cached));
}
const originalJson = res.json.bind(res);
res.json = (data) => {
client.setEx(key, ttl, JSON.stringify(data));
return originalJson(data);
};
next();
} catch (err) {
next();
}
};
}
app.get('/api/users', cacheMiddleware(60), async (req, res) => {
const users = await User.find();
res.json({ users });
});
Example 2: Optimized posts query
async function getPostsWithAuthors() {
const posts = await Post.find();
for (const post of posts) {
post.author = await User.findById(post.author);
}
return posts;
}
async function getPostsWithAuthorsOptimized() {
return Post.find()
.populate('author', 'name email')
.select('title content author createdAt')
.lean()
.limit(20);
}
Example 3: Image optimization with sharp
npm install sharp
const sharp = require('sharp');
const fs = require('fs');
async function optimizeImage(inputPath, outputPath) {
await sharp(inputPath)
.resize(800, 600, { fit: 'inside', withoutEnlargement: true })
.jpeg({ quality: 80 })
.toFile(outputPath);
const inputSize = fs.statSync(inputPath).size;
const outputSize = fs.statSync(outputPath).size;
console.log(`Size reduction: ${((1 - outputSize / inputSize) * 100).toFixed(2)}%`);
}
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('image'), async (req, res) => {
const inputPath = req.file.path;
const outputPath = `optimized/${req.file.filename}.jpg`;
await optimizeImage(inputPath, outputPath);
res.json({ path: outputPath });
});
9. Compression
gzip
npm install compression
const compression = require('compression');
app.use(compression());
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) return false;
return compression.filter(req, res);
},
level: 6
}));
Typical savings: HTML 70–90%, JSON 60–80%, CSS/JS 50–70%.
10. Common pitfalls
Event loop blocking
// Bad: CPU-heavy work on main thread
app.get('/heavy', (req, res) => {
let sum = 0;
for (let i = 0; i < 1e9; i++) sum += i;
res.json({ sum });
});
// Better: worker thread
const { Worker } = require('worker_threads');
app.get('/heavy', (req, res) => {
const worker = new Worker('./heavy-task.js');
worker.on('message', (result) => res.json({ sum: result }));
worker.on('error', (err) => res.status(500).json({ error: err.message }));
});
// heavy-task.js
const { parentPort } = require('worker_threads');
let sum = 0;
for (let i = 0; i < 1e9; i++) sum += i;
parentPort.postMessage(sum);
Memory leaks
Causes: Global accumulators, listeners never removed, unbounded caches.
const EventEmitter = require('events');
const emitter = new EventEmitter();
// Bad: listener added every request
app.get('/api/data', (req, res) => {
emitter.on('data', (data) => res.json(data));
});
// Better: remove after use
app.get('/api/data', (req, res) => {
const handler = (data) => {
res.json(data);
emitter.off('data', handler);
};
emitter.on('data', handler);
});
11. Practical tips
Request timing
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
if (duration > 1000) {
console.warn(`Slow: ${req.method} ${req.url} ${duration}ms`);
}
});
next();
});
Centralized async errors
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get('/api/users', asyncHandler(async (req, res) => {
const users = await User.find();
res.json({ users });
}));
app.use((err, req, res, next) => {
logger.error(err.message, { stack: err.stack });
res.status(500).json({ error: 'Internal Server Error' });
});
Keep-Alive
const http = require('http');
const server = http.createServer((req, res) => res.end('Hello'));
server.keepAliveTimeout = 65000;
server.headersTimeout = 66000;
server.listen(3000);
Summary
Takeaways
- Clustering: Cluster module or PM2
-ifor multi-core - Caching: Redis, in-memory TTL, invalidate on writes
- Database: Indexes, avoid N+1, connection pools,
lean()where appropriate - Async:
Promise.all, limit concurrency withp-limit - Memory: Streams, LRU caches, monitor RSS/heap
- Compression:
compressionmiddleware, optimize images
Optimization priority
- Database queries (often the largest bottleneck)
- Caching (high impact, moderate effort)
- Async parallelism
- Compression (network)
- Clustering (throughput)
Tools
| Tool | Use |
|---|---|
--prof | CPU samples |
| Chrome DevTools | Heap and CPU |
| clinic.js | Holistic diagnosis |
| autocannon | HTTP load tests |
| PM2 | Process metrics |
Next steps
- Deeper Node.js security hardening
- Microservices patterns
Resources
Related posts
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Node.js performance guide: use clustering for multi-core, Redis and in-memory caching, fix N+1 queries, profiling with -…
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- Node.js Async Programming: Callbacks, Promises, and
- Getting Started with Node.js: Install, Setup, and Hello
- Node.js Module System: CommonJS and ES Modules Explained
Keywords Covered in This Article (Related Search Terms)
This article covers Node.js, Performance, Clustering, Caching, Profiling, Benchmarking.