Performance & Profiling

TypeScript Real World Applications — Performance & Profiling
TypeScript Real World Applications
Course 3 · Chapter 8 · Performance & Profiling

⚡ Performance & Profiling

A type-safe app can still be a slow one — the compiler has no opinion on memory leaks or CPU-bound loops. This chapter covers finding real bottlenecks with a profiler instead of guessing, the memory-leak patterns that sneak in through the very abstractions this course has been building (event buses, caches, containers), and a couple of typed optimization techniques.

Memory Leaks: Where They Actually Hide

In a garbage-collected language, a "leak" means something is still reachable long after it should be — usually because something is holding a reference it forgot to release:

Forgotten Listeners

Chapter 7's TypedEventBus.on() without a matching off() keeps every subscribed callback (and everything it closes over) alive forever.

Unbounded Caches

A Map used as a cache that never evicts entries grows without limit — every unique key ever seen stays in memory.

Closures Over Large Objects

A callback that closes over a big object (even if it only uses one small field) keeps the entire object alive as long as the callback exists.

Singleton Containers

Chapter 1's singleton lifetime is exactly right for a connection pool — but registering something request-scoped as a singleton by mistake leaks that request's data forever.

// A subtle leak: subscribing per-request without ever unsubscribing app.get("/dashboard", (req, res) => { bus.on("order.placed", (payload) => { // closes over `res` — after this request ends, `res` (and this listener) // is still referenced by the bus, forever, for every request that ever hit this route. res.write(JSON.stringify(payload)); }); // ❌ never calls bus.off() — the listener outlives the request });

🔥 CPU Profiling

Don't guess where time is going — measure it. Node ships a built-in CPU profiler; Chrome DevTools reads the output as a flame graph:

node --prof server.js # ... generate load against the running server ... node --prof-process isolate-0x*-v8.log > profile.txt # or, for a live interactive view: node --inspect server.js # then open chrome://inspect in Chrome and record a CPU profile

Flame Graph

Width = time spent. The widest bars, not the deepest stack, tell you what's actually expensive.

Self Time vs Total Time

"Self time" is time in that function alone; "total time" includes everything it calls — a function can have huge total time but tiny self time.

Common Bottlenecks in a TypeScript/Node App

Node is single-threaded for JavaScript execution — anything synchronous and slow blocks every other request on the event loop, the same event loop covered in the Node.js Fundamentals course:

Blocking vs Non-Blocking

Blocks the Event Loop
const sorted = hugeArray.sort(compareFn); // synchronous, O(n log n) on the main thread const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, "sha512");

Every other request waits until these finish.

Yields Back to the Event Loop
const hash = await new Promise<Buffer>((resolve, reject) => { crypto.pbkdf2(password, salt, 100000, 64, "sha512", (err, key) => err ? reject(err) : resolve(key)); }); // or offload CPU-heavy work to a worker_thread entirely

Other requests keep being handled while this runs.

Also worth checking: N+1 database queries (Chapter 6), JSON.stringify on very large objects, and synchronous file I/O (fs.readFileSync) on a hot request path.

🧮 Optimization Strategies

Typed Memoization

Caching a pure function's results is one of the highest-value, lowest-risk optimizations — and generics keep it reusable across any function shape:

A Generic Memoize Function

function memoize<Args extends unknown[], Result>( fn: (...args: Args) => Result ): (...args: Args) => Result { const cache = new Map<string, Result>(); return (...args: Args): Result => { const key = JSON.stringify(args); if (cache.has(key)) { return cache.get(key)!; } const result = fn(...args); cache.set(key, result); return result; }; } function expensiveCalculation(n: number): number { let total = 0; for (let i = 0; i < n; i++) total += i; return total; } const memoizedCalc = memoize(expensiveCalculation); memoizedCalc(1_000_000); // slow the first time memoizedCalc(1_000_000); // instant — served from cache, typed as number, not any

Args extends unknown[]

A tuple type parameter — memoize works for any function signature while keeping every argument's type intact.

Cache Growth = New Leak Risk

An unbounded memoization cache trades a CPU problem for a memory one — pair it with an LRU eviction policy for anything called with unbounded input.

💻 Coding Challenges

Challenge 1: Find and Fix a Listener Leak

Given a route handler that calls bus.on() on every request without ever calling bus.off(), rewrite it to unsubscribe once the response finishes (hint: the res object emits a "finish" event).

Goal: Practice matching every subscription with an unsubscription tied to a clear lifetime.

→ Solution

Challenge 2: Move Blocking Work Off the Event Loop

Given a route handler that calls a synchronous, CPU-heavy function directly, rewrite it to run that work in a worker_thread so it no longer blocks other requests.

Goal: Practice recognizing and fixing an event-loop-blocking bottleneck.

→ Solution

Challenge 3: Add Eviction to a Memoize Cache

Extend the memoize function above with a maximum cache size — once the limit is reached, evict the least-recently-used entry before adding a new one.

Goal: Practice trading a CPU optimization for a bounded, safe memory cost instead of an unbounded one.

→ Solution

⚠️ Gotcha: Optimizing Before Measuring

It's tempting to memoize everything, cache everything, and rewrite the "obviously slow" loop — before ever running a profiler. Most of the time the actual bottleneck is somewhere unexpected (a synchronous crypto call, an N+1 query, a huge JSON.stringify), and the "obvious" hot path was fine all along. Profile first, then optimize the function that the flame graph actually points to.

🎯 What's Next

With the app fast and leak-free, the final chapter ships it: Deployment & CI/CD — containerization, automated testing in pipelines, canary deployments, and rollback strategies.