Express.js Performance Optimization

Metrics, optimization techniques, and production best practices for fast, scalable APIs

Performance Optimization Best Practices

Table of Contents

1. Performance

What it is: The speed, responsiveness, and efficiency of your application. Performance measures how quickly your system processes requests, handles concurrency, and utilizes resources (CPU, memory, I/O). Good performance means low latency, high throughput, and efficient resource usage.

Why it matters

  • User experience: 100ms delay = 1% drop in conversion
  • SEO: Google ranks faster sites higher
  • Cost: Efficient code needs fewer servers
  • Scalability: Better performance = handle more users with same resources
  • Retention: 53% of mobile users abandon sites over 3 seconds

Key performance metrics

MetricDescriptionGood TargetCritical for
TTFBTime to first byte< 200msServer response
LCPLargest content paint< 2.5sPerceived load
FIDFirst input delay< 100msInteractivity
CLSLayout shift< 0.1Visual stability
QPSQueries per second1000+Scalability
Error rateFailed requests< 0.1%Reliability

Measuring performance in Express:

JavaScript — Response time middlewareapp.use((req, res, next) => { const start = process.hrtime(); res.on('finish', () => { const [seconds, nanoseconds] = process.hrtime(start); const duration = seconds * 1000 + nanoseconds / 1000000; console.log(`${req.method} ${req.url} - ${res.statusCode} - ${duration.toFixed(2)}ms`); if (duration > 1000) { logger.warn('Slow response', { url: req.url, duration: duration.toFixed(2), method: req.method, user: req.user?.id }); } }); next(); }); const metrics = { requests: 0, errors: 0, totalTime: 0, slowRequests: 0 }; app.use((req, res, next) => { metrics.requests++; const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; metrics.totalTime += duration; if (duration > 1000) metrics.slowRequests++; if (res.statusCode >= 400) metrics.errors++; if (metrics.requests % 100 === 0) { console.log('Performance metrics:', { requests: metrics.requests, avgResponseTime: (metrics.totalTime / metrics.requests).toFixed(2) + 'ms', errorRate: ((metrics.errors / metrics.requests) * 100).toFixed(2) + '%', slowRate: ((metrics.slowRequests / metrics.requests) * 100).toFixed(2) + '%' }); } }); next(); }); app.get('/metrics', (req, res) => { res.json({ uptime: process.uptime(), memory: process.memoryUsage(), cpu: process.cpuUsage(), requests: { total: metrics.requests, errors: metrics.errors, avgResponseTime: (metrics.totalTime / metrics.requests).toFixed(2), slowRequests: metrics.slowRequests } }); });

Load testing simulation:

JavaScriptasync function loadTest(endpoint, concurrency, durationSeconds) { const results = { total: 0, successes: 0, failures: 0, responseTimes: [] }; const endTime = Date.now() + durationSeconds * 1000; const workers = []; for (let i = 0; i < concurrency; i++) { workers.push(async () => { while (Date.now() < endTime) { const start = Date.now(); try { await fetch(endpoint); results.successes++; results.responseTimes.push(Date.now() - start); } catch (error) { results.failures++; } results.total++; } }); } await Promise.all(workers.map(w => w())); const avgTime = results.responseTimes.reduce((a, b) => a + b, 0) / results.responseTimes.length; const p95 = results.responseTimes.sort((a, b) => a - b)[Math.floor(results.responseTimes.length * 0.95)]; console.log('Load test results:', { totalRequests: results.total, successRate: ((results.successes / results.total) * 100).toFixed(2) + '%', avgResponseTime: avgTime.toFixed(2) + 'ms', p95ResponseTime: p95 + 'ms', rps: (results.total / durationSeconds).toFixed(2) }); } // loadTest('http://localhost:3000/api/users', 50, 30);

2. Optimization

What it is: The process of identifying and eliminating bottlenecks to improve application speed, efficiency, and resource usage. Optimization spans database queries, code execution, network calls, memory usage, and asset delivery.

Key optimization areas

AreaImpactCommon fixesImprovement
DatabaseHighIndexes, query optimization, connection pooling10–100x
CachingHighRedis, CDN, in-memory cache10–1000x
CodeMediumAlgorithm optimization, async patterns2–10x
NetworkMediumCompression, batching, CDN2–5x
MemoryLow to MediumStreaming, pooling, cleanup1.5–2x

Database optimization:

JavaScript — Fix N+1 queries// ❌ Bad: N+1 queries app.get('/users-with-posts-bad', async (req, res) => { const users = await db.user.findMany(); for (const user of users) { user.posts = await db.post.findMany({ where: { userId: user.id } }); } res.json(users); }); // ✅ Good: Single query with JOIN app.get('/users-with-posts-good', async (req, res) => { const users = await db.user.findMany({ include: { posts: true } }); res.json(users); }); // ✅ Best: Selective loading + pagination app.get('/users-with-posts-best', async (req, res) => { const users = await db.user.findMany({ take: 20, include: { posts: { take: 5, orderBy: { createdAt: 'desc' } } }, select: { id: true, name: true, email: true, posts: { select: { id: true, title: true, createdAt: true } } } }); res.json(users); }); // Index optimization CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_posts_user_id_created ON posts(user_id, created_at); CREATE INDEX idx_orders_user_status_date ON orders(user_id, status, created_at);

Connection pooling:

JavaScript — PostgreSQL poolconst { Pool } = require('pg'); const pool = new Pool({ host: process.env.DB_HOST, port: 5432, database: 'myapp', user: 'user', password: 'password', max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, maxUses: 7500 }); async function query(sql, params) { const client = await pool.connect(); try { return await client.query(sql, params); } finally { client.release(); } } setInterval(() => { console.log('Pool stats:', { total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount }); }, 60000);

Code optimization — algorithm efficiency:

JavaScript — O(n²) vs O(n)// ❌ Bad: O(n²) nested loops app.get('/duplicate-users-bad', async (req, res) => { const users = await db.user.findMany(); const duplicates = []; for (let i = 0; i < users.length; i++) { for (let j = i + 1; j < users.length; j++) { if (users[i].email === users[j].email) duplicates.push(users[i]); } } res.json(duplicates); }); // ✅ Good: O(n) with hash map app.get('/duplicate-users-good', async (req, res) => { const users = await db.user.findMany(); const emailMap = new Map(); const duplicates = []; for (const user of users) { if (emailMap.has(user.email)) duplicates.push(user); else emailMap.set(user.email, user); } res.json(duplicates); });

Memory optimization — streaming:

JavaScript// ❌ Bad: Load entire dataset into memory app.get('/export-users-bad', async (req, res) => { const users = await db.user.findMany(); const csv = users.map(u => `${u.id},${u.name},${u.email}`).join('\n'); res.send(csv); }); // ✅ Good: Stream data app.get('/export-users-good', async (req, res) => { res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', 'attachment; filename=users.csv'); const stream = await db.user.findMany({ cursor: { id: lastId }, take: 1000 }); for await (const batch of stream) { res.write(batch.map(u => `${u.id},${u.name},${u.email}`).join('\n')); } res.end(); }); const fs = require('fs'); const readline = require('readline'); async function processLargeFile(filePath) { const rl = readline.createInterface({ input: fs.createReadStream(filePath) }); let lineCount = 0; for await (const line of rl) { await processLine(line); if (++lineCount % 1000 === 0) console.log(`Processed ${lineCount} lines`); } }

Network optimization:

JavaScript — Compression & batchingconst compression = require('compression'); app.use(compression({ level: 6, threshold: 1024, filter: (req, res) => { if (req.headers['x-no-compression']) return false; return compression.filter(req, res); } })); // ❌ Bad: Multiple round trips for (let id of [1, 2, 3, 4, 5]) await fetch(`/api/users/${id}`); // ✅ Good: Batch request const users = await fetch('/api/users/batch', { method: 'POST', body: JSON.stringify({ ids: [1, 2, 3, 4, 5] }) }); app.post('/api/users/batch', async (req, res) => { const { ids } = req.body; const users = await db.user.findMany({ where: { id: { in: ids } } }); res.json(users); });

Gzip compression with custom responses:

JavaScriptconst zlib = require('zlib'); const util = require('util'); const gzip = util.promisify(zlib.gzip); app.get('/api/large-data', async (req, res) => { const data = await fetchLargeDataset(); if (req.acceptsEncodings('gzip')) { const compressed = await gzip(JSON.stringify(data)); res.set('Content-Encoding', 'gzip'); res.set('Content-Type', 'application/json'); return res.send(compressed); } res.json(data); });

Asset optimization middleware:

JavaScript — Static assets & Sharpapp.use('/static', express.static('public', { maxAge: '1y', etag: true, lastModified: true, setHeaders: (res, path) => { if (path.endsWith('.html')) res.setHeader('Cache-Control', 'no-cache'); else if (path.match(/\.(jpg|png|gif|svg)$/)) { res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); } else if (path.match(/\.(css|js)$/)) { res.setHeader('Cache-Control', 'public, max-age=86400'); } } })); const sharp = require('sharp'); app.get('/image/:id', async (req, res) => { const { width = 800, quality = 80, format = 'webp' } = req.query; const imagePath = await getImagePath(req.params.id); let pipeline = sharp(imagePath); if (width) pipeline = pipeline.resize(parseInt(width)); if (format === 'webp') pipeline = pipeline.webp({ quality: parseInt(quality) }); const optimizedImage = await pipeline.toBuffer(); res.set('Content-Type', `image/${format}`); res.set('Cache-Control', 'public, max-age=86400'); res.send(optimizedImage); });

3. Best Practices

What they are: Proven, industry-standard guidelines and patterns that lead to maintainable, secure, scalable, and performant applications. Best practices represent collective wisdom from thousands of production deployments.

Why they matter

  • Maintainability: Easier to debug, test, and extend
  • Security: Avoid common vulnerabilities
  • Team productivity: Consistent code patterns
  • Reliability: Fewer bugs and edge cases
  • Onboarding: New developers understand code faster

Project structure best practices:

Project layoutsrc/ ├── config/ # Configuration files │ ├── database.js │ ├── redis.js │ └── index.js ├── controllers/ # Request handlers ├── services/ # Business logic ├── models/ # Data models ├── middleware/ # Express middleware ├── utils/ # Helper functions ├── routes/ # API routes │ ├── v1/ │ └── index.js ├── tests/ # Unit & integration tests └── app.js # App initialization

Code organization — separation of concerns:

JavaScript// ✅ Good: Separation of concerns router.post('/', validateOrder, authMiddleware, async (req, res, next) => { try { const order = await orderService.createOrder(req.user.id, req.body); res.status(201).json(order); } catch (error) { next(error); } }); function validateOrder(req, res, next) { const schema = Joi.object({ items: Joi.array().items(Joi.object({ productId: Joi.string().required(), quantity: Joi.number().min(1).required() })).min(1).required() }); const { error } = schema.validate(req.body); if (error) return res.status(400).json({ error: error.details[0].message }); next(); } class OrderService { async createOrder(userId, orderData) { const user = await this.validateUser(userId); const items = await this.validateProducts(orderData.items); const total = this.calculateTotal(items); await this.checkBalance(user, total); const order = await this.createOrderRecord(userId, items, total); await this.deductBalance(userId, total); await this.sendConfirmation(user.email, order); return order; } }

Error handling best practices:

JavaScriptclass AppError extends Error { constructor(message, statusCode, isOperational = true) { super(message); this.statusCode = statusCode; this.isOperational = isOperational; this.timestamp = new Date().toISOString(); Error.captureStackTrace(this, this.constructor); } } class NotFoundError extends AppError { constructor(resource) { super(`${resource} not found`, 404); this.name = 'NotFoundError'; } } const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; app.get('/api/users/:id', asyncHandler(async (req, res) => { const user = await userService.findById(req.params.id); if (!user) throw new NotFoundError('User'); res.json(user); })); app.use((err, req, res, next) => { logger.error(err.message, { stack: err.stack, url: req.url, userId: req.user?.id }); if (err.isOperational) { return res.status(err.statusCode).json({ error: err.message, code: err.name, timestamp: err.timestamp }); } res.status(500).json({ error: 'Internal server error', requestId: req.id }); });

Security best practices:

JavaScriptconst rateLimit = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100, message: 'Too many requests from this IP', keyGenerator: (req) => req.user?.id || req.ip }); app.use('/api/', limiter); const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, skipSuccessfulRequests: true }); app.post('/api/login', authLimiter, loginHandler); const mongoSanitize = require('express-mongo-sanitize'); const xss = require('xss-clean'); app.use(mongoSanitize()); app.use(xss()); app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", 'https://trusted-cdn.com'] } }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true } })); // ✅ Parameterized queries const user = await db.query('SELECT * FROM users WHERE email = $1', [userEmail]);

Environment configuration:

JavaScript — config/index.jsconst dotenv = require('dotenv'); dotenv.config(); const config = { env: process.env.NODE_ENV || 'development', server: { port: parseInt(process.env.PORT) || 3000, corsOrigin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:3000'] }, database: { url: process.env.DATABASE_URL, poolSize: parseInt(process.env.DB_POOL_SIZE) || 10 }, auth: { jwtSecret: process.env.JWT_SECRET, jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d' }, validate() { const required = ['DATABASE_URL', 'JWT_SECRET']; const missing = required.filter(key => !process.env[key]); if (missing.length) throw new Error(`Missing: ${missing.join(', ')}`); if (process.env.NODE_ENV === 'production' && process.env.JWT_SECRET.length < 32) { throw new Error('JWT_SECRET must be at least 32 characters in production'); } } }; config.validate(); module.exports = config;

Database best practices:

JavaScript — Transactions & soft deletesapp.post('/api/transfer', async (req, res) => { const { fromAccount, toAccount, amount } = req.body; const transaction = await db.$transaction(async (prisma) => { const from = await prisma.account.findUnique({ where: { id: fromAccount } }); if (from.balance < amount) throw new Error('Insufficient funds'); await prisma.account.update({ where: { id: fromAccount }, data: { balance: { decrement: amount } } }); await prisma.account.update({ where: { id: toAccount }, data: { balance: { increment: amount } } }); return { success: true }; }); res.json(transaction); }); async function softDeleteUser(userId, deletedBy) { return await db.user.update({ where: { id: userId }, data: { deletedAt: new Date(), deletedBy, email: `deleted_${Date.now()}_${userId}@deleted.com` } }); } const activeUsers = await db.user.findMany({ where: { deletedAt: null } });

API design best practices:

JavaScript — RESTful naming// ✅ Good GET /api/users GET /api/users/123 POST /api/users PUT /api/users/123 PATCH /api/users/123 DELETE /api/users/123 GET /api/users/123/posts function successResponse(res, data, statusCode = 200) { return res.status(statusCode).json({ success: true, data, timestamp: new Date().toISOString(), requestId: req.id }); } function errorResponse(res, message, statusCode = 400, details = null) { return res.status(statusCode).json({ success: false, error: message, details, timestamp: new Date().toISOString(), requestId: req.id }); }

Testing best practices:

JavaScript — Jest & Supertestdescribe('UserService', () => { test('should create user successfully', async () => { const userData = { name: 'John', email: 'john@example.com' }; mockDb.user.create.mockResolvedValue({ id: 1, ...userData }); const result = await userService.createUser(userData); expect(result).toEqual({ id: 1, ...userData }); }); }); describe('User API', () => { test('GET /api/users should return paginated users', async () => { const response = await request(app) .get('/api/users?page=1&limit=10') .expect(200); expect(response.body).toHaveProperty('data'); expect(response.body).toHaveProperty('pagination'); }); });

Performance best practices checklist:

JavaScript — Cluster & productionconst cluster = require('cluster'); const os = require('os'); if (cluster.isMaster) { const numCPUs = os.cpus().length; for (let i = 0; i < numCPUs; i++) cluster.fork(); cluster.on('exit', (worker) => cluster.fork()); } else { app.listen(3000); } if (process.env.NODE_ENV === 'production') { app.set('trust proxy', 1); app.enable('view cache'); } process.on('SIGTERM', () => { server.close(() => process.exit(0)); });

Summary Checklist

Performance Checklist ✅

  • Implement response time monitoring
  • Add database indexes for frequent queries
  • Use connection pooling
  • Enable compression (gzip/brotli)
  • Implement caching (Redis, CDN)
  • Optimize N+1 queries
  • Use streaming for large datasets
  • Enable HTTP/2 or HTTP/3
  • Minify and compress assets
  • Use CDN for static files

Optimization Checklist ✅

  • Profile and identify bottlenecks
  • Optimize algorithm complexity (O(n²) → O(n))
  • Implement pagination for all lists
  • Use batch operations instead of loops
  • Enable keep-alive connections
  • Implement database read replicas
  • Use message queues for heavy tasks
  • Optimize images (WebP, lazy loading)

Best Practices Checklist ✅

  • Use environment variables for config
  • Implement proper error handling
  • Add request validation
  • Use HTTPS in production
  • Implement rate limiting
  • Add security headers (helmet)
  • Sanitize user input
  • Use parameterized queries
  • Implement logging and monitoring
  • Write unit and integration tests
  • Use semantic versioning
  • Document API with OpenAPI/Swagger
  • Implement CI/CD pipeline
  • Regular dependency updates
  • Use TypeScript or JSDoc for typing

Pro Tips

  • Measure before optimizing: Use profiling tools (clinic, node --prof)
  • The 80/20 rule: 80% of improvements come from 20% of bottlenecks
  • Premature optimization is evil: Optimize only when you have data
  • Production profiling: Never optimize without real-world metrics
  • Document decisions: Why you chose certain optimizations or patterns