MFormations
Modern Backend Engineering

Chapitre 11

Chapitre 11 — Performance Backend

Chapitre 11 — Performance Backend

Cours — Performance Backend

1. Profiling et Instrumentation

Pourquoi profiler ?

Avant d'optimiser, il faut mesurer. Le profiling identifie les goulots d'étranglement.

Node.js — Profiling avec Clinic.js

# Installation
npm install -g clinic

# Profiling
clinic doctor -- node server.js

# Flamegraph
clinic flame -- node server.js

# Heap profiler
clinic heapprofiler -- node server.js

Utilisation de --prof (V8)

node --prof server.js
# Analyse
node --prof-process isolate-*.log > profiled.txt

Python — cProfile et py-spy

import cProfile
import pstats

profiler = cProfile.Profile()
profiler.enable()
# code à profiler
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats('cumtime').print_stats(20)

py-spy (sampling profiler, sans modification du code) :

py-spy record -o profile.svg --pid 12345
py-spy top --pid 12345

Go — pprof

import _ "net/http/pprof"

// Dans le code
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()

// Analyse
go tool pprof http://localhost:6060/debug/pprof/heap
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

2. Caching : Stratégies

Types de cache

TypeLatenceCapacitéPersistance
L1 (CPU)~1nsKBNon
In-memory (RAM)~100nsGBNon
Redis~1msGBOptionnelle
CDN~10msTBOui
Database~10msTBOui

Cache-Aside Pattern

async function getUser(id) {
  // 1. Vérifier le cache
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);

  // 2. Charger depuis la BDD
  const user = await db.users.findById(id);

  // 3. Stocker dans le cache
  await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);

  return user;
}

Write-Through Cache

async function updateUser(id, data) {
  // 1. Écrire dans la BDD
  const user = await db.users.update(id, data);

  // 2. Mettre à jour le cache
  await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);

  return user;
}

Invalidation strategies

  • TTL : expiration automatique (le plus simple)
  • Event-driven : invalidation sur modification
  • Write-through : mise à jour synchrone du cache
  • Lazy invalidation : invalidation à la lecture si stale

Redis Cache Patterns

// Rate limiting avec sliding window
const WINDOW = 60; // secondes
const MAX = 100;   // requêtes

async function checkRateLimit(userId) {
  const key = `ratelimit:${userId}:${Math.floor(Date.now() / 1000 / WINDOW)}`;
  const count = await redis.incr(key);
  if (count === 1) await redis.expire(key, WINDOW);
  return count <= MAX;
}

CDN Caching

Cache-Control: public, max-age=31536000, immutable
CDN-Cache-Control: public, max-age=86400
Cloudflare-CDN-Cache-Control: public, max-age=86400

3. Database Optimization

Indexing

-- Index simple
CREATE INDEX idx_users_email ON users(email);

-- Index composé (ordre des colonnes important !)
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at DESC);

-- Index partiel
CREATE INDEX idx_active_users ON users(is_active) WHERE is_active = true;

-- Index hash pour égalité
CREATE INDEX idx_users_email_hash ON users USING HASH(email);

Query Optimization

-- Mauvais : pas d'index, full scan
SELECT * FROM orders WHERE YEAR(created_at) = 2026;

-- Bon : index sur created_at
SELECT * FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';

N+1 Query Problem

// MAUVAIS : N+1 queries
const users = await db.users.findAll();
for (const user of users) {
  const posts = await db.posts.findAll({ where: { userId: user.id } });
}

// BON : eager loading (SQL JOIN)
const users = await db.users.findAll({
  include: [{ model: db.posts }]
});

// OU : batch loading (DataLoader)
const userLoader = new DataLoader(ids =>
  db.users.findAll({ where: { id: ids } })
);

Pagination efficace

-- Offset-based (lent pour les grandes pages)
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 10000;

-- Cursor-based (rapide)
SELECT * FROM users WHERE id > 10000 ORDER BY id LIMIT 20;

4. Connection Pooling

Principe

Réutiliser les connexions à la base de données plutôt que d'en créer une nouvelle à chaque requête.

PostgreSQL (pg-pool)

import { Pool } from 'pg';

const pool = new Pool({
  max: 20,               // max connexions
  idleTimeoutMillis: 30000, // temps avant fermeture idle
  connectionTimeoutMillis: 2000, // timeout création
  maxUses: 7500,         // recycle après X requêtes
});

// Toujours libérer la connexion
async function query(text, params) {
  const client = await pool.connect();
  try {
    return await client.query(text, params);
  } finally {
    client.release();
  }
}

Monitoring du pool

setInterval(() => {
  console.log({
    totalCount: pool.totalCount,
    idleCount: pool.idleCount,
    waitingCount: pool.waitingCount,
  });
}, 5000);

5. Lazy Loading et Pagination

Lazy Loading (Cursors)

async function* getUserEvents(userId, batchSize = 100) {
  let cursor = null;
  while (true) {
    const [events, nextCursor] = await db.events.findByCursor({
      userId,
      limit: batchSize,
      cursor
    });
    if (events.length === 0) break;
    yield events;
    cursor = nextCursor;
  }
}

// Usage
for await (const batch of getUserEvents(userId)) {
  processBatch(batch);
}

Offset vs Cursor

CritèreOffsetCursor
Performance (grandes pages)DégradéConstant
Données en temps réelRésultats dupliquésStable
Pagination aléatoireOuiNon
Index requisNonOui

6. Benchmarking

wrk

wrk -t12 -c400 -d30s http://localhost:3000/api/users
# Résultat :
# Requests/sec:  15243.67
# Transfer/sec:    3.45MB
# Latency (ms):  avg=26.15  max=542.37

autocannon (Node.js)

npx autocannon -c 100 -d 30 http://localhost:3000/api/users

Go Benchmark

// bench_test.go
func BenchmarkCalculateHash(b *testing.B) {
    for i := 0; i < b.N; i++ {
        CalculateHash("test-input")
    }
}
go test -bench=. -benchmem
# Result: 12345 ns/op  512 B/op  6 allocs/op

7. Memory Leaks et GC

Causes communes de memory leaks

  1. Variables globales : accumulées dans le temps
  2. Closures : capture de références non libérées
  3. Timers/Callbacks : non nettoyés
  4. Event listeners : attachés sans détachement
  5. Caches sans limite : accumulation infinie
  6. Streams non drainés : backpressure ignoré

Détection (Node.js)

// Heap dump
import heapdump from 'heapdump';
heapdump.writeSnapshot('/tmp/heap-1.heapsnapshot');

// Comparaison
heapdump.writeSnapshot('/tmp/heap-2.heapsnapshot');
// Analyser avec Chrome DevTools → Memory → Load snapshot

WeakRef et FinalizationRegistry

// WeakRef pour cache sans fuite mémoire
const cache = new Map();

function getCached(key) {
  const ref = cache.get(key);
  if (ref) {
    const value = ref.deref();
    if (value !== undefined) return value;
  }
  const value = computeExpensive(key);
  cache.set(key, new WeakRef(value));
  return value;
}

GC Tuning (Node.js)

# Voir le GC
node --trace-gc server.js

# Augmenter la mémoire
node --max-old-space-size=4096 server.js

# GC explicite (déconseillé en prod)
global.gc();

8. Network Optimization

Compression

import compression from 'express-compression';

app.use(compression({
  brotli: { enabled: true, quality: 11 },
  gzip: { level: 6 }
}));

HTTP/2 Multiplexing

import http2 from 'node:http2';

const server = http2.createSecureServer({ key, cert }, app);
server.listen(3000);

Keep-Alive

const agent = new http.Agent({
  keepAlive: true,
  keepAliveMsecs: 1000,
  maxSockets: 256,
  maxFreeSockets: 64,
});

9. Async et Concurrence

Worker Threads (Node.js)

import { Worker } from 'node:worker_threads';

function runInWorker(data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js', { workerData: data });
    worker.on('message', resolve);
    worker.on('error', reject);
  });
}

CPU-bound tasks

// Ne PAS bloquer l'event loop
function heavyComputation() {
  // MAUVAIS - bloque l'event loop
  for (let i = 0; i < 1e9; i++) { /* ... */ }

  // BON - découper en chunks
  const chunkSize = 1000;
  for (let i = 0; i < 1e9; i += chunkSize) {
    setImmediate(() => processChunk(i, chunkSize));
  }
}

10. Monitoring des Performances

APM Tools

  • Datadog APM : tracing distribué
  • New Relic : monitoring applicatif
  • OpenTelemetry : standard open-source
  • Prometheus + Grafana : métriques personnalisées

Métriques clés

import prometheus from 'prom-client';

const httpRequestDuration = new prometheus.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests',
  labelNames: ['method', 'route', 'status'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
});

app.use((req, res, next) => {
  const end = httpRequestDuration.startTimer();
  res.on('finish', () => {
    end({ method: req.method, route: req.route?.path, status: res.statusCode });
  });
  next();
});

Database Query Monitoring

const dbQueryDuration = new prometheus.Histogram({
  name: 'db_query_duration_seconds',
  help: 'Database query duration',
  labelNames: ['query', 'table'],
  buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
});

async function monitoredQuery(text, params) {
  const end = dbQueryDuration.startTimer();
  try {
    return await pool.query(text, params);
  } finally {
    end({ query: text.split(' ')[0], table: extractTable(text) });
  }
}