NodeJSOptimization Best Practices

Optimization Best Practices

This page distills the whole Performance section into an actionable checklist — the concrete habits that keep a Node application fast. The unifying theme: Node's single thread must stay unblocked, and the cheapest work is work you don't do. None of this replaces measuring first — apply these where a profile or load test shows they'll help — but they're the patterns that separate a snappy service from a sluggish one.

Never block the event loop

JS
// ❌ Synchronous APIs block the single thread — every request waits:
const data = fs.readFileSync('big.json')           // blocks
const hash = crypto.pbkdf2Sync(pw, salt, 100000, 64, 'sha512')   // blocks ~100ms

// ✅ Use the async variants so the loop stays free:
const data = await fs.promises.readFile('big.json')
const hash = await promisify(crypto.pbkdf2)(pw, salt, 100000, 64, 'sha512')

// ✅ For unavoidable CPU-bound work, move it OFF the main thread:
import { Worker } from 'node:worker_threads'
// ...run the heavy computation in a worker, keep the event loop responsive.
Sync APIs and heavy CPU work on the main thread stall *every* concurrent request — the #1 Node mistake
Because all requests share one thread, any synchronous operation — `fs.readFileSync`, `crypto.*Sync`, `JSON.parse` of a huge payload, a tight loop, catastrophic regex backtracking — freezes the *entire* server for its duration, not just the current request. Always prefer the async APIs, and for genuinely CPU-bound work (image processing, large crypto, heavy computation) offload to [worker threads](/nodejs/worker-threads) or a separate service so the main loop keeps accepting and dispatching requests. This single principle is the root of most Node performance advice: keep the loop free.
Optimize the database — usually the biggest win
  • Fix N+1 queries — fetch related data in one query (join / IN) instead of one query per row in a loop.

  • Add indexes for the columns you filter and sort on — a missing index turns a fast lookup into a full table scan.

  • Select only the columns you needSELECT * ships data you discard and defeats covering indexes.

  • Use connection pooling — reusing connections avoids per-request handshake cost.

  • Paginate large result sets — never load 100k rows to show 20.

  • Push work to the database — aggregate/filter in SQL rather than fetching everything and processing in Node.

The database is the bottleneck in most web apps — fix N+1 and indexes before micro-optimizing JS
Before tuning a line of JavaScript, look at the database — it dominates request time in the vast majority of web applications. The two highest-impact fixes are eliminating **N+1 queries** (the pattern where you query once per item in a list instead of once for the whole list) and adding **indexes** to the columns you filter and join on. A single missing index can make an endpoint 100× slower under real data volume. These database wins typically dwarf anything you can achieve by optimizing application code, so always profile the query layer first.
Do less work — cache and parallelize

JS
// Parallelize independent async work:
const [user, orders] = await Promise.all([getUser(id), getOrders(id)])

// Cache expensive, frequently-read results (with a TTL + size bound):
const cached = await redis.get(key)
if (cached) return JSON.parse(cached)

// Stream large responses instead of buffering them fully in memory:
fs.createReadStream('huge.csv').pipe(res)
The fastest work is work avoided — cache results and run independent operations in parallel
Two patterns from earlier in this section pay off everywhere. [Caching](/nodejs/caching-strategies) eliminates repeated expensive work — a result computed once and reused costs almost nothing on subsequent requests. Running independent async operations with `Promise.all` instead of sequential `await`s collapses their latency to the slowest single operation rather than the sum. And [streaming](/nodejs/streams-intro) large payloads keeps memory flat and lets bytes flow to the client immediately, instead of buffering an entire file or result set in memory first. Together these often deliver bigger gains than any code-level micro-optimization.
Right-size the runtime and deployment
  • Run in cluster mode or behind a process manager — one Node process uses one CPU core; fork one per core to use the whole machine.

  • Set NODE_ENV=production — many libraries (Express, React) skip dev-only overhead when this is set.

  • Tune --max-old-space-size to the container's memory so GC behaves and you avoid surprise OOM kills.

  • Keep dependencies lean — fewer, lighter packages mean faster startup and smaller attack surface.

  • Offload static assets and compression to a CDN/proxy so Node spends cycles on application logic.

  • Reuse connections — HTTP keep-alive and pooled DB/HTTP clients avoid repeated setup cost.

One Node process uses one core — forgetting to cluster wastes most of a multi-core machine
A single Node process runs your JavaScript on one thread and therefore one CPU core, no matter how many cores the machine has. On an 8-core box, a single process leaves ~87% of the CPU idle. Use the [cluster module](/nodejs/cluster-module) or a process manager (PM2) to run one worker per core, or run multiple container replicas behind a load balancer — this is often the single biggest throughput multiplier available and requires no code changes for stateless apps. Pair it with `NODE_ENV=production`, which switches many popular libraries out of their slower development modes.
The optimization checklist

Area

Check

Event loop

No sync I/O / crypto on the hot path; CPU work offloaded to workers

Database

No N+1; indexes present; pooled connections; paginated; select only needed columns

Caching

Hot reads cached with TTL + size bound; high hit rate; correct invalidation

Concurrency

Independent awaits parallelized with Promise.all

Payload

Compression on text; pagination; streaming for large bodies

Runtime

NODE_ENV=production; clustered across cores; memory limits tuned

Verification

Measured before/after with a profile and a load test

Measure, fix the biggest bottleneck, measure again — then stop at 'good enough'
Every item above is a *candidate*, not a mandate — apply it where measurement shows it helps. The discipline that ties the section together: establish a baseline, [profile](/nodejs/profiling) to find the dominant cost, fix that one thing, [load test](/nodejs/load-testing) to confirm the gain, and repeat until you hit your target. Then stop — performance work has steep diminishing returns, and readable code that's "fast enough" beats a tangled micro-optimized mess. Optimize the common path, verify with data, and resist tuning what no one will notice.
Next
Use every core on the machine: [Concurrency in Node.js](/nodejs/concurrency-intro).