# The performance traps in generated JavaScript

> Source: https://learn-javascript.org/review/performance/
> Part of Learn JavaScript, free to read.

JavaScript's performance story is different from every other language here because of one structural fact: **in Node, blocking is not slow — it is downtime.** One request that occupies the event loop for two seconds makes every concurrent user wait two seconds.

Generated code is written for clarity and is correct. What it omits is the awareness that the runtime is single-threaded.

:::note Measure it directly
```bash
node --cpu-prof --cpu-prof-dir=./prof app.js     # load it in Chrome DevTools
node --inspect app.js                            # live profiling and heap snapshots
npx clinic doctor -- node app.js                 # tells you which category you have
```
`clinic doctor` is the fastest first step: it categorises the problem as event loop, I/O, memory or CPU before you go looking.
:::

## Blocking the event loop

### 1. Synchronous I/O in a request path

```js
app.get("/config", (req, res) => {
  const cfg = JSON.parse(fs.readFileSync("./config.json", "utf8"));   // blocks everyone
  res.json(cfg);
});
```

Every `*Sync` function in a handler blocks the whole process. Common offenders: `readFileSync`, `existsSync`, `execSync`, `crypto.pbkdf2Sync`, `zlib.gunzipSync`.

At startup, `*Sync` is fine and often clearer. In a request path it is never fine.

**Catch it with:** `grep -rn 'Sync(' src/routes src/handlers` — a targeted grep beats a lint rule here, because the same functions are legitimate at module load.

### 2. Large `JSON.parse` and `JSON.stringify`

Both are synchronous and both are O(n). A 50 MB payload parses for hundreds of milliseconds with everything else stopped.

For large documents use a streaming parser; for large responses, stream the serialisation rather than building one string.

### 3. CPU-bound work in the main thread

Image processing, hashing, compression, big sorts. Move it to a `worker_threads` pool, or out of the process entirely.

```js
import { Worker } from "node:worker_threads";
// or, for a quick win, a pool: piscina
```

### 4. Catastrophic regex

A generated regex with nested quantifiers against untrusted input hangs the event loop. It appears under performance and under [security](/review/security/) for the same reason — in Node the two are the same failure.

## Async shapes

### 5. Sequential awaits that should be parallel

```js
const user = await getUser(id);          // three round trips, one after another
const orders = await getOrders(id);
const prefs = await getPrefs(id);
```

```js
const [user, orders, prefs] = await Promise.all([getUser(id), getOrders(id), getPrefs(id)]);
```

Correct either way; a third of the latency in the second. **Look for:** consecutive `await` lines with no data dependency between them.

### 6. `await` inside a loop

```js
for (const id of ids) results.push(await fetchUser(id));   // 100 sequential trips
```

The fix is `Promise.all` over `.map` — but not unbounded, which is the next item.

### 7. Unbounded concurrency

```js
await Promise.all(ids.map(fetchUser));    // 50,000 simultaneous requests
```

The overcorrection from item 6, and it is worse: file descriptor exhaustion, an immediate 429 storm, retries piling on top.

```js
async function mapLimit(items, limit, fn) {
  const out = new Array(items.length);
  let i = 0;
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
    while (i < items.length) { const idx = i++; out[idx] = await fn(items[idx], idx); }
  }));
  return out;
}
```

Eight lines and no dependency. Generated fan-out code essentially never includes a limit.

### 8. `Promise.all` where `allSettled` was meant

`Promise.all` rejects on the first failure and abandons the rest — but the work already started keeps running, so you pay for it and discard it. For "fetch these ten things and show me what you got", `allSettled`.

## Memory

### 9. Listeners never removed

```js
emitter.on("data", handler);              // in a function called per request
```

Each call adds a listener that is never removed, so the emitter's array grows forever and each captured closure keeps its scope alive. Node's `MaxListenersExceededWarning` is the tell — treat it as a leak, not as noise.

The same applies to `setInterval` without `clearInterval`, and to `AbortController` listeners in long-lived objects.

### 10. Unbounded caches

```js
const cache = new Map();                  // grows until the process dies
cache.set(key, value);
```

Generated caching code is a bare `Map` or object with no eviction. Use an LRU with a size bound, or `WeakMap`/`WeakRef` when the key's lifetime should govern.

### 11. Closures holding large objects

```js
function makeHandler(hugeBuffer) {
  return () => doSomethingSmall(hugeBuffer.length);   // retains the whole buffer
}
```

The closure keeps everything in scope alive. Extract what you need before capturing.

**Find leaks with:** two heap snapshots in DevTools, compare, sort by retained size. The dominant retainer is usually one of these three.

## Data access

### 12. N+1 queries

```js
for (const order of orders) {
  order.items = await db.items.findMany({ where: { orderId: order.id } });
}
```

Correct, passes tests with three orders, 101 round trips in production. Fetch with `IN`/`ANY` and group in memory; with Prisma, `include`.

**Catch it with:** a test asserting query count. It is the only reliable defence and the highest-value performance test in a service.

### 13. Array operations chained over large data

```js
items.filter(f).map(m).filter(g).map(n)      // four full passes, three intermediate arrays
```

Fine at a thousand items, meaningful at a million. One `reduce` or a plain loop for hot paths — but do not do this pre-emptively; the readable version is right until profiling says otherwise.

### 14. Repeated `array.includes` in a loop

O(n) inside O(n). `new Set(array)` once, outside the loop. Same class as [the Python version](https://learn-python.com/review/performance/) and equally common.

## Startup and delivery

### 15. Everything imported at module load

```js
import { hugeLibrary } from "huge-library";   // parsed and executed at startup
```

For a serverless function this is cold-start latency on every invocation. Dynamic `import()` for anything used on a minority of paths.

### 16. Bundle size in the browser

A single unnecessary dependency can double a bundle. `npx source-map-explorer` or your bundler's analyser tells you where the weight is, and the [dependency table](/review/dependencies/) removes a surprising amount of it.

## The reviewer's shortcut

Three questions on any diff:

1. **Is anything synchronous and slow in a request path?** (`*Sync`, big `JSON.parse`, a heavy regex)
2. **Is any `await` in a loop, and is any `Promise.all` unbounded?**
3. **Does anything add a listener, an interval, or a cache entry without a matching removal?**

Those catch items 1–3, 5–7, and 9–11 — most of the real cost here.

:::verdict The Node-specific mental model
There is one CPU and everyone shares it. Any question of the form "is this slow?" is really "does this block, and for how long?" — and a 200 ms block under 100 concurrent requests is a 20-second tail. That framing catches more than any profiler.
:::

## Common questions

### How do I know if the event loop is blocked?

`clinic doctor` tells you directly, and `perf_hooks`'s `monitorEventLoopDelay()` gives you a histogram you can put on a dashboard. Event-loop lag at p99 is one of the two or three most useful metrics a Node service can export.

### Are worker threads worth the complexity?

For genuinely CPU-bound work that must stay in-process, yes — and use a pool rather than spawning per task, since worker startup is not free. For most services the better answer is moving that work out to a queue and a separate process entirely.

### Is `Promise.all` bad?

No — it is right for a handful of independent operations. It becomes a problem when the array is user-sized rather than code-sized. The rule: if the length comes from data rather than from the source code, bound the concurrency.

### Should I optimise array chains pre-emptively?

No. `filter().map()` is clearer and the difference is irrelevant below tens of thousands of elements. Profile first; the readable version is correct until measurement says otherwise.
