DevPath · Learn to code ESPTEN

Observability and performance

Performance: caching, lazy loading and the DB

The golden rule: the fastest work is the work you do not do

Optimizing performance is, almost always, avoiding repeated work: not recomputing, not asking again, not sending what is already there. The central tool for that is the cache, and it helps to think of it in tiers.

Tiered caching

  1. Browser / HTTP: the client stores responses based on the Cache-Control and ETag headers. If the resource has not changed, the server replies 304 Not Modified and does not resend the body. Network cost: zero.

  2. CDN for static assets: a network of servers close to the user serves images, JS and CSS from the edge, without touching your origin. Closer = less latency and less load on your backend.

  3. Backend cache (Redis) with the cache-aside pattern:

    is it in the cache?  → yes → return it (fast)
                         → no  → go to the DB, store the result in the cache, return it
    

    The application manages the cache "on the side" of the DB. You have to decide the TTL (how long the data lives) and invalidate the entry when the data changes, or you will serve stale data. Invalidating a cache is one of the truly hard problems.

On the frontend: send less and later

In the database: indexes and the N+1 problem

The DB is usually the bottleneck. Two classics:

Measure before optimizing. "I think this is slow" is not data; a p95 and a trace are. Optimizing blindly adds complexity with no guarantee of improvement.

Examples

Cache-aside with a Map: do not recompute what is already computed

function cache(fn) {
  const store = new Map();
  return function (key) {
    if (store.has(key)) return store.get(key);
    const value = fn(key);
    store.set(key, value);
    return value;
  };
}

let calls = 0;
const square = cache((n) => { calls++; return n * n; });
square(8); square(8); square(8);
console.log("result:", square(8), "actual calls:", calls); // 64, 1

N+1 vs. batching: 1 query instead of N

// Simulates a "bulk" load of several ids at once.
function loadUsers(ids) {
  console.log("DB queries:", 1, "for", ids.length, "ids");
  return ids.map((id) => ({ id, name: "User " + id }));
}

const ids = [1, 2, 3, 4, 5];
const users = loadUsers(ids); // 1 trip, not 5
console.log(users.length, "users loaded");
Put this into practice

DevPath is a hands-on course: you read the theory here; in the app you put it into practice with exercises that really run, offline.

Start free in the app →
← The three pillars of observabilityScaling: horizontal, load balancers and stateless →