본문으로 건너뛰기 Node.js Performance: Clustering, Caching, and Profiling

Node.js Performance: Clustering, Caching, and Profiling

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:
  1. Measure first: Do not guess—profile and measure
  2. Find bottlenecks: Improve the slowest parts first
  3. Iterate: Change one thing at a time
  4. 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

  1. Clustering: Cluster module or PM2 -i for multi-core
  2. Caching: Redis, in-memory TTL, invalidate on writes
  3. Database: Indexes, avoid N+1, connection pools, lean() where appropriate
  4. Async: Promise.all, limit concurrency with p-limit
  5. Memory: Streams, LRU caches, monitor RSS/heap
  6. Compression: compression middleware, optimize images

Optimization priority

  1. Database queries (often the largest bottleneck)
  2. Caching (high impact, moderate effort)
  3. Async parallelism
  4. Compression (network)
  5. Clustering (throughput)

Tools

ToolUse
--profCPU samples
Chrome DevToolsHeap and CPU
clinic.jsHolistic diagnosis
autocannonHTTP load tests
PM2Process metrics

Next steps

  • Deeper Node.js security hardening
  • Microservices patterns

Resources



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.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Node.js, Performance, Clustering, Caching, Profiling, Benchmarking.