Asynchronous performance optimization techniques
Asynchronous Performance Optimization Techniques
Node.js's asynchronous non-blocking I/O model is the core of its high performance, but improper asynchronous code writing can actually lead to performance degradation. Properly leveraging asynchronous features requires mastering the event loop mechanism, task scheduling strategies, and resource management techniques.
Understanding Event Loop Phases
The Node.js event loop consists of six main phases:
- Timers: Executes setTimeout and setInterval callbacks
- Pending callbacks: Executes system operation callbacks (e.g., TCP errors)
- Idle/Prepare: Internal use
- Poll: Retrieves new I/O events and executes related callbacks
- Check: Executes setImmediate callbacks
- Close callbacks: Executes close event callbacks (e.g., socket.on('close'))
// Demonstrating phase execution order
setImmediate(() => console.log('Check phase'));
setTimeout(() => console.log('Timers phase'), 0);
fs.readFile(__filename, () => {
console.log('Poll phase');
setImmediate(() => console.log('Nested Check phase'));
});
Task Queue Priority Control
Microtasks (Promise, process.nextTick) have the highest priority and will jump the queue:
Promise.resolve().then(() => console.log('Microtask 1'));
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('Microtask 2'));
// Output order: nextTick → Microtask 1 → Microtask 2
Optimization suggestions:
- Avoid excessive use of process.nextTick in hot paths
- Time-consuming synchronous operations should be wrapped in setImmediate to release the event loop
Streaming Large Datasets
Traditional approaches cause memory spikes:
// Anti-pattern
fs.readFile('huge.log', (err, data) => {
const lines = data.toString().split('\n'); // Memory peak
processLines(lines);
});
Switch to stream processing:
fs.createReadStream('huge.log')
.pipe(split2()) // Split by line
.on('data', line => processLine(line))
.on('end', () => console.log('Processing complete'));
Cluster Mode for Multi-Core CPU Utilization
A single-threaded Node.js instance cannot fully utilize multiple cores:
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
// Create child processes based on CPU cores
os.cpus().forEach(() => cluster.fork());
} else {
require('./app'); // Each child process runs an application instance
}
Asynchronous Concurrency Control
Unlimited concurrency leads to resource exhaustion:
// Dangerous example
urls.forEach(url => fetch(url).then(processResponse));
Use p-limit for concurrency control:
const limit = require('p-limit');
const concurrency = 10;
const limiter = limit(concurrency);
Promise.all(
urls.map(url =>
limiter(() => fetch(url).then(processResponse))
)
);
Memory Leak Prevention
Common asynchronous code leak scenarios:
// Closure holding reference to large object
server.on('request', (req) => {
const hugeObj = loadHugeData();
req.on('close', () => {
// hugeObj cannot be released
});
});
Solutions:
- Use WeakMap instead of strong references
- Explicitly clean up event listeners
Timer Optimization Techniques
Avoid timer accumulation:
// Wrong approach
function pollUpdates() {
fetchUpdates().then(() => {
setTimeout(pollUpdates, 100); // May overlap execution
});
}
Improved solution:
let isUpdating = false;
async function pollUpdates() {
if (isUpdating) return;
isUpdating = true;
await fetchUpdates();
isUpdating = false;
setTimeout(pollUpdates, 100);
}
Asynchronous Stack Traces
Default asynchronous errors lose call stacks:
function foo() {
setTimeout(() => {
throw new Error('Hard-to-track error');
}, 100);
}
Enable async_hooks for enhanced tracing:
const async_hooks = require('async_hooks');
const fs = require('fs');
async_hooks.createHook({
init(asyncId, type, triggerAsyncId) {
fs.writeSync(1, `${type}(${asyncId}): trigger: ${triggerAsyncId}\n`);
}
}).enable();
Promise Performance Pitfalls
Chained Promises create too many microtasks:
// Inefficient approach
users.reduce((promise, user) => {
return promise.then(() => updateUser(user));
}, Promise.resolve());
Batch processing optimization:
const BATCH_SIZE = 100;
for (let i = 0; i < users.length; i += BATCH_SIZE) {
await Promise.all(
users.slice(i, i+BATCH_SIZE).map(updateUser)
);
}
Asynchronous Resource Management
Unreleased resources cause file descriptor leaks:
// Forgotten database connection
const db = await connectDB();
app.get('/', async () => {
return db.query('SELECT...');
});
Use AsyncDisposable (Node.js 20+):
const { open } = require('node:fs/promises');
async function processFile() {
await using file = await open('data.txt');
// File automatically closed
}
Worker Threads for CPU-Intensive Tasks
Offload computations to worker threads:
const { Worker } = require('worker_threads');
function runInWorker(script, data) {
return new Promise((resolve) => {
const worker = new Worker(script, { workerData: data });
worker.on('message', resolve);
});
}
// worker.js
const { workerData, parentPort } = require('worker_threads');
parentPort.postMessage(heavyCompute(workerData));
Event Emitter Optimization
High-frequency events require special handling:
const { EventEmitter } = require('events');
class Sensor extends EventEmitter {
constructor() {
super();
this.setMaxListeners(100); // Avoid memory leak warnings
this.cache = new Map();
}
onData(data) {
if (!this.cache.has(data.id)) {
this.emit('new-data', data); // Deduplicated triggering
this.cache.set(data.id, Date.now());
}
}
}
Asynchronous Initialization Pattern
Parallel initialization during application startup:
async function initApp() {
const [db, config, cache] = await Promise.all([
connectDatabase(),
loadConfig(),
initCache()
]);
return { db, config, cache };
}
Real-Time Application Optimization
WebSocket message batching:
const messageQueue = [];
let isProcessing = false;
socket.on('message', (msg) => {
messageQueue.push(msg);
if (!isProcessing) {
isProcessing = true;
setImmediate(processQueue);
}
});
function processQueue() {
const batch = messageQueue.splice(0, 100);
if (batch.length) {
saveToDB(batch).finally(() => {
if (messageQueue.length) setImmediate(processQueue);
else isProcessing = false;
});
}
}
Error Handling Strategies
Uncaught Promises lead to silent failures:
// Global Promise error catching
process.on('unhandledRejection', (err) => {
metrics.increment('unhandled_rejection');
logger.error('Unhandled rejection', err);
});
// Context-preserving error wrapper
async function wrapAsync(fn) {
return async (...args) => {
try {
return await fn(...args);
} catch (err) {
err.context = { args };
throw err;
}
};
}
Performance Analysis Tools
Use async_hooks to monitor asynchronous latency:
const hooks = require('async_hooks');
const active = new Map();
hooks.createHook({
init(id, type, triggerId) {
if (type === 'Timeout') {
active.set(id, {
start: process.hrtime.bigint(),
triggerId
});
}
},
destroy(id) {
const record = active.get(id);
if (record) {
const duration = Number(process.hrtime.bigint() - record.start) / 1e6;
console.log(`Timeout took ${duration}ms`);
active.delete(id);
}
}
}).enable();
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn