<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Performance Angular, Nestjs et Node.js : Le Guide définitif 2026

Une application full-stack lente n'est presque jamais la faute d'un "framework lent". C'est presque toujours la somme de dix petites mauvaises décisions : un *ngFor sans trackBy, une requête sans index, un point de terminaison qui ne compresse pas la réponse, un pool de connexions de taille aléatoire. Pris individuellement, ils ressemblent à des détails. Ensemble, ils font la différence entre une application qui répond en 80 ms et une autre qui prend 2 secondes sous charge.

Ce guide rassemble les techniques que j'utilise réellement en production sur la pile Angular + NestJS + Node.js + PostgreSQL + Redis, avec un code complet, des chiffres de référence réalistes et les erreurs les plus courantes que j'ai vues (et commises) au cours des dernières années.

Index

  1. Pourquoi la performance est importante
  2. Comment fonctionne le flux requête-réponse
  3. Prérequis et pile de référence
  4. Optimiser angulaire
  5. Optimiser NestJS
  6. Optimiser Node.js au niveau de l'exécution
  7. Base de données : PostgreSQL, index et pooling de connexions
  8. Projet réel : Tableau de bord avec cache Redis
  9. Optimisations avancées
  10. Benchmark : avant et après
  11. Sécurité
  12. Bonnes pratiques architecturales
  13. 20 erreurs courantes
  14. FAQ
  15. Conclusion

Pourquoi la performance est importante

Qu'est-ce que c'est, en pratique. Optimiser les performances d'une stack Angular/NestJS/Node.js revient à réduire trois nombres : le temps perçu par l'utilisateur (Core Web Vitals côté frontend), le temps de réponse du backend (p50/p95/p99), et les ressources consommées pour servir chaque requête (CPU, mémoire, connexions). DB).

Pourquoi c'est important. Google utilise Core Web Vitals dans le classement. Amazon a mesuré que chaque tranche de 100 ms de latence supplémentaire coûte des points de pourcentage de conversion. Mais la raison la plus concrète, celle que je vois tous les jours, en est une autre : un backend qui n’évolue pas de manière linéaire vous oblige à acheter du matériel au lieu d’écrire un meilleur code – et à un moment donné, le matériel ne suffit plus

.

Quand l'appliquer. Pas immédiatement. Optimiser prématurément un point de terminaison appelé 10 fois par jour est une perte de temps. Les techniques de ce guide doivent être appliquées lorsque : vous disposez de données de profilage réelles (et non de suppositions), le trafic augmente ou un point de terminaison spécifique apparaît dans les journaux comme un goulot d'étranglement.

Avantages. Temps de réponse réduits, coûts d'infrastructure réduits, meilleur référencement, réduction du taux de désabonnement des utilisateurs, capacité à gérer les pics de trafic sans temps d'arrêt.

Inconvénients. Chaque optimisation a un coût : plus de complexité (cache à invalider, Workers à orchestrer), plus de surface de bugs, plus de temps de développement. Une mise en cache mal gérée est la principale cause des données obsolètes affichées aux utilisateurs.

Erreurs courantes. Optimiser sans mesurer au préalable ; copier des techniques à partir de blogs sans comprendre les compromis ; ignorer la base de données (la première cause de lenteur dans la pile NestJS) tout en passant des semaines sur des micro-optimisations angulaires.

Cas d'usage réels. Un e-commerce qui passe de 3s à 400ms sur la page produit augmentant les conversions de 15% ; un tableau de bord interne qui expire avec 200 utilisateurs simultanés car le pool Postgres est défini sur 5 connexions ; une API publique qui gère un trafic 10x après avoir ajouté Redis devant les requêtes les plus lourdes.

Comment fonctionne le flux requête-réponse

Avant d'optimiser, vous avez besoin d'un modèle mental de l'endroit où le temps est réellement passé dans une pile Angular → NestJS → Postgres/Redis.

┌─────────────┐     ┌──────────────┐     ┌─────────────┐     ┌────────────┐
│   Browser   │────▶│  CDN / Nginx │────▶│   NestJS    │────▶│ PostgreSQL │
│  (Angular)  │◀────│ (compressione│◀────│ (Node.js)   │◀────│  + Redis   │
└─────────────┘     │  + cache)    │     └─────────────┘     └────────────┘
      │                                        │  │
      │ Change detection                       │  └─▶ Worker threads (CPU-bound)
      │ Lazy loading                           │
      │ Bundle splitting                       └─▶ Cluster / PM2 (multi-core)

Diagramme de séquence pour une requête type (par exemple GET /api/products?page=2) :

sequenceDiagram
    participant U as Utente
    participant A as Angular
    participant N as Nginx/CDN
    participant S as NestJS
    participant R as Redis
    participant P as PostgreSQL

    U->>A: Naviga verso /products
    A->>N: HTTP GET /api/products?page=2
    N->>N: Cache HTT check / compressione
    N->>S: Forward richiesta
    S->>S: Guard, Pipe, Interceptor
    S->>R: GET products:page:2
    alt Cache HIT
        R-->>S: Dati in cache (< 1ms)
    else Cache MISS
        S->>P: SELECT ... LIMIT 20 OFFSET 20
        P-->>S: Righe (5-50ms)
        S->>R: SET products:page:2 (TTL 60s)
    end
    S-->>N: JSON response
    N-->>A: Response (compressa, gzip/br)
    A->>A: Change detection + render
    A-->>U: UI aggiornata

Chaque flèche de ce diagramme est un endroit où vous pouvez perdre – ou gagner – des millisecondes. Les sections suivantes les attaquent un à un.

Prérequis et pile de référence

Pour suivre les exemples de ce guide, vous avez besoin de :

Outil Version recommandée (2026) Remarques
Node.js 22 LTS ou supérieur Prise en charge native des threads de travail et de la récupération
Angulaire 19+ Signaux stables, sans zone en option
NestJS 11+ Support Fastify comme adaptateur alternatif à Express
PostgreSQL 16+ Meilleures statistiques de planification, requêtes parallèles
Redis 7+ Prise en charge des fonctions granulaires et des ACL
Docker / Docker Compose dernière stabilité Environnement local reproductible
pnpm 9+ Installation plus rapide que npm, économe en disque
Prisme ou TypeORM dernière écurie ORM tapé

Vous n'avez pas besoin de tout rassembler dès le premier jour. Si vous lisez pour optimiser un projet existant, la partie base de données (index, pooling) doit presque toujours être appliquée avant tout le reste : c'est là que se cache le plus grand gain avec le moins de risques.

Installation rapide de l'environnement de référence

# 1. Crea il progetto NestJS
npx @nestjs/cli new backend --package-manager pnpm

# 2. Aggiungi le dipendenze di performance
cd backend
pnpm add @nestjs/platform-fastify ioredis @nestjs/cache-manager cache-manager
pnpm add @nestjs/throttler helmet compression class-validator class-transformer
pnpm add @prisma/client
pnpm add -D prisma

# 3. Inizializza Prisma (schema + client tipizzato)
npx prisma init

# 4. Angular: crea il frontend
npx @angular/cli new frontend --style=scss --routing --ssr

# 5. Docker Compose per Postgres + Redis in locale
# docker-compose.yml
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: app
    ports: ["5432:5432"]
    volumes: ["pgdata:/var/lib/postgresql/data"]

  redis:
    image: redis:7-alpine
    ports: ["6379:6379"]

volumes:
  pgdata:

@nestjs/platform-fastify remplace Express en tant que moteur HTTP : avec la même logique d'application, Fastify gère environ 20 à 30 % de requêtes en plus par seconde grâce à un routeur et une analyse JSON plus efficaces — cela vaut la peine de l'adopter sur de nouveaux projets.

Optimiser angulaire

Détection de changement et signaux sans zone

Le problème historique d'Angular est que zone.js intercepte chaque événement asynchrone (clic, timer, fetch) et relance la détection de changement sur toute l'arborescence des composants. Sur les applications volumineuses, cela signifie des milliers de contrôles inutiles pour un seul clic.

Les signaux résolvent le problème à la racine : le framework sait exactement quel composant dépend de quelles données, et ne met à jour que cela.

import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-cart-summary',
  standalone: true,
  template: `
    <p>Articoli: {{ itemCount() }}</p>
    <p>Totale: {{ total() | currency }}</p>
  `,
})
export class CartSummaryComponent {
  private items = signal<{ price: number; qty: number }[]>([]);

  itemCount = computed(() => this.items().length);
  total = computed(() =>
    this.items().reduce((sum, i) => sum + i.price * i.qty, 0)
  );

  addItem(item: { price: number; qty: number }) {
    this.items.update(list => [...list, item]);
  }
}

Pourquoi ça marche : computed() ne recalcule que lorsque le signal dont il dépend change, et Angular ne met à jour que les nœuds DOM liés à ce signal, sans parcourir l'intégralité du composant. Dans une application avec zone.js classique, le même addItem aurait déclenché la détection de modifications sur l'ensemble du sous-arbre du composant parent.

Bootstrap sans zone (Angulaire 18+) :

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';

bootstrapApplication(AppComponent, {
  providers: [provideZonelessChangeDetection()],
});

Avantages : bundle plus petit (zone.js pèse ~ 30 Ko), moins de cycles de détection de modifications, débogage plus prévisible. Inconvénients : Les bibliothèques tierces qui attendent zone.js (certaines anciennes versions de bibliothèques de matériaux ou de graphiques) peuvent nécessiter un NgZone.run() manuel. Erreur commune : mélange de signaux et de référence d'objet mutable — si vous mutez un tableau avec .push() au lieu de .update(), le signal ne détecte pas le changement.

trackBy et @for avec la clé

@Component({
  template: `
    @for (product of products(); track product.id) {
      <app-product-card [product]="product" />
    }
  `,
})
export class ProductListComponent {
  products = signal<Product[]>([]);
}

La nouvelle syntaxe @for requiert un track, contrairement à l'ancienne *ngFor où trackBy était facultatif et souvent oublié. Sans clé stable, Angular détruit et recrée chaque nœud DOM à chaque mise à jour de la liste au lieu de réorganiser ceux existants — sur une table de 500 lignes, c'est la différence entre une mise à jour instantanée et un clic visible.

Chargement paresseux avancé et préchargement stratégique

// app.routes.ts
export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () =>
      import('./features/dashboard/dashboard.component')
        .then(m => m.DashboardComponent),
  },
  {
    path: 'admin',
    loadChildren: () => import('./features/admin/admin.routes').then(m => m.ADMIN_ROUTES),
    canActivate: [adminGuard],
  },
];

// app.config.ts — preload solo le route probabili, non tutte
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
import { QuicklinkStrategy } from 'ngx-quicklink'; // preload solo i link visibili in viewport

providers: [
  provideRouter(routes, withPreloading(QuicklinkStrategy)),
]

PreloadAllModules est pratique mais naïf : il télécharge tout en arrière-plan même si l'utilisateur n'y arrive jamais. Une stratégie basée sur une fenêtre d'affichage (comme Quicklink, inspirée de Google) précharge uniquement les formulaires liés par des éléments réellement visibles, ce qui réduit le gaspillage de bande passante sur mobile.

SSR et hydratation progressive

// app.config.server.ts
import { provideServerRendering } from '@angular/platform-server';
import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser';

export const serverConfig = [
  provideServerRendering(),
  provideClientHydration(withIncrementalHydration()),
];
<!-- component che si idrata solo quando entra in viewport -->
@defer (on viewport) {
  <app-heavy-chart [data]="chartData()" />
} @placeholder {
  <div class="chart-skeleton"></div>
} @loading (minimum 200ms) {
  <app-spinner />
}

Avantages SSR : Première peinture presque instantanée, idéale pour le référencement et les Core Web Vitals (LCP). Inconvénients : coût CPU côté serveur pour le rendu, nécessité de gérer du code qui s'exécute à la fois sur Node et dans le navigateur (rien window n'est regardé). @defer avec sur la fenêtre d'affichage est la technique la plus efficace en 2026 pour les composants lourds (graphiques, éditeurs de texte enrichi, cartes) qui ne sont pas nécessaires pour le premier rendu.

Analyse groupée

ng build --configuration production --stats-json
npx webpack-bundle-analyzer dist/frontend/stats.json

Cible réaliste pour une application d'entreprise : bundle initial de moins de 150 Ko gzippé. Les causes les plus courantes de gonflement du bundle : importation de bibliothèques entières au lieu de fonctions individuelles (import _ from 'lodash' au lieu de import anti-bounce from 'lodash/debounce'), moment.js au lieu de date-fns, icônes SVG importées en tant que composants au lieu de sprites.

Optimiser NestJS

Intercepteur de compression et de mise en cache HTTP

// main.ts
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import compression from '@fastify/compress';
import helmet from '@fastify/helmet';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
  );

  await app.register(compression, { encodings: ['gzip', 'br'] });
  await app.register(helmet);

  await app.listen(3000, '0.0.0.0');
}
bootstrap();

La compression Brotli (br) réduit les charges utiles JSON typiques de 70 à 80 % par rapport au gzip standard, à un coût CPU légèrement plus élevé en compression — acceptable pour la plupart des API REST.

Cache avec Redis et gestionnaire de cache

// cache.module.ts
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { redisStore } from 'cache-manager-redis-yet';

@Module({
  imports: [
    CacheModule.registerAsync({
      isGlobal: true,
      useFactory: async () => ({
        store: await redisStore({
          socket: { host: process.env.REDIS_HOST, port: 6379 },
          ttl: 60_000, // 60s default
        }),
      }),
    }),
  ],
})
export class AppCacheModule {}
// products.controller.ts
import { CacheInterceptor, CacheTTL, CacheKey } from '@nestjs/cache-manager';
import { UseInterceptors } from '@nestjs/common';

@Controller('products')
@UseInterceptors(CacheInterceptor)
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @Get()
  @CacheKey('products_list')
  @CacheTTL(60)
  findAll(@Query('page') page = 1) {
    return this.productsService.findAll(+page);
  }
}

Que mettre en cache : réponses publiques, lues bien plus souvent qu'écrites (catalogues produits, listes d'articles de blog, configurations). Ce qu'il ne faut jamais mettre en cache au niveau de la couche HTTP : Les réponses contenant des données spécifiques à l'utilisateur authentifié sans clé de cache incluant l'ID utilisateur — est l'erreur de sécurité la plus courante avec CacheInterceptor.

Invalidation explicite en cas de modification des données :

async update(id: string, dto: UpdateProductDto) {
  const product = await this.repo.save({ id, ...dto });
  await this.cacheManager.del('products_list');
  await this.cacheManager.del(`product:${id}`);
  return product;
}

Grandes réponses en streaming

@Get('export')
async exportProducts(@Res() res: FastifyReply) {
  res.raw.writeHead(200, { 'Content-Type': 'text/csv' });

  const cursor = this.repo.createQueryBuilder('p').stream();
  res.raw.write('id,name,price\n');

  cursor.on('data', row => res.raw.write(`${row.p_id},${row.p_name},${row.p_price}\n`));
  cursor.on('end', () => res.raw.end());
}

Pour exporter des milliers de lignes, charger le tout en mémoire avec find() puis sérialiser en JSON peut faire exploser le tas du processus Node. Le streaming écrit ligne par ligne en gardant l'utilisation de la mémoire constante, quelle que soit la taille de l'ensemble de données.

Tuyau de validation : faites attention au coût

app.useGlobalPipes(new ValidationPipe({
  whitelist: true,
  forbidNonWhitelisted: true,
  transform: true,
  // disabilita su endpoint interni ad altissimo traffico se il payload è già fidato
}));

class-validator avec transform: true utiliser reflect-metadata et réflexion au moment de l'exécution : sur les points de terminaison appelés des dizaines de milliers de fois par seconde (par exemple, webhooks internes, ingestion d'événements), le le coût de validation peut devenir mesurable. Dans ces cas spécifiques, une validation manuelle légère (ou zod avec un schéma précompilé) est plus rapide.

Optimiser Node.js au niveau de l'exécution

Boucle d'événement : ne la bloquez jamais

// ❌ Blocca l'event loop: nessun'altra richiesta viene servita nel frattempo
function hashPasswordSync(password: string) {
  return crypto.pbkdf2Sync(password, 'salt', 100_000, 64, 'sha512');
}

// ✅ Sposta il lavoro CPU-bound fuori dal thread principale
import { Worker } from 'node:worker_threads';

function hashPasswordAsync(password: string): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./hash-worker.js', { workerData: { password } });
    worker.once('message', resolve);
    worker.once('error', reject);
  });
}

Nœud. js est monothread pour le code JavaScript : une fonction synchrone coûteuse (hachage, analyse de fichiers volumineux, compression personnalisée) bloque toutes requêtes en cours, pas seulement celle qui l'a générée. Les threads de travail déplacent le travail lié au processeur vers des threads séparés, laissant la boucle d'événements libre pour servir les E/S.

Mode cluster : utiliser tous les cœurs

// cluster.ts
import cluster from 'node:cluster';
import os from 'node:os';

if (cluster.isPrimary) {
  const cpus = os.availableParallelism();
  console.log(`Avvio ${cpus} worker`);
  for (let i = 0; i < cpus; i++) cluster.fork();

  cluster.on('exit', (worker) => {
    console.log(`Worker ${worker.process.pid} morto, riavvio`);
    cluster.fork();
  });
} else {
  import('./main'); // bootstrap NestJS
}

En production je préfère déléguer cela à PM2 (pm2 start dist/main.js -i max) ou à l'orchestrateur (réplicas Kubernetes) plutôt que de gérer les clusters à la main : même avantage, moins de code à maintenir.

Gestion mémoire : détecter les fuites

node --inspect dist/main.js
# Chrome DevTools → Memory → Heap Snapshot, confronta due snapshot a distanza di tempo
// Pattern comune di leak: listener non rimossi
class NotificationService {
  constructor(private emitter: EventEmitter) {
    // ❌ ogni nuova istanza aggiunge un listener che non viene mai rimosso
    this.emitter.on('notify', this.handleNotify);
  }
}

Les fuites de mémoire dans Node proviennent presque toujours de : des écouteurs d'événements non supprimés, un cache en mémoire sans limite de taille (utilisez lru-cache avec un max, jamais un Map qui grandit indéfiniment), fermeture contenant des références à de gros objets, minuteries (setInterval) jamais effacées.

Threads de travail pour un véritable travail lié au processeur

Table de décision :

Scénario Solution
Requête de base de données, appel HTTP externe async/await normal (lié aux E/S, ne bloque pas)
Hachage de mot de passe, analyse PDF, traitement d'image Thread de travail ou file d'attente de tâches (BullMQ)
Des milliers de petits calculs répétés Pool de threads de travail (par ex. piscine)
Tâches longues (minutes), non urgentes File d'attente de tâches asynchrone (Redis + BullMQ), thread non-worker en ligne

Base de données : PostgreSQL, index et pooling de connexions

C'est, d'après mon expérience, la section qui vaut plus que toutes les autres réunies. La plupart des problèmes de performances que j'ai résolus en production se trouvaient ici, pas dans le frontend.

Regroupement de connexions corrigé

// prisma schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
# ❌ Pool troppo piccolo: richieste in coda sotto carico
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=5"

# ✅ Dimensionato sul numero di core del backend e sul limite di Postgres
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=20&pool_timeout=10"

Règle générale : connection_limit par instance backend ≈ (cœurs de processeur du serveur Postgres × 2) / nombre de répliques backend. Avec plusieurs répliques NestJS partageant le même Postgres, utilisez PgBouncer devant la base de données en mode de pooling transaction pour éviter d'épuiser le nombre maximal de connexions Postgres (100 par défaut).

# pgbouncer.ini (estratto)
[databases]
app = host=postgres port=5432 dbname=app

[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25

Indices : l'optimisation la plus efficace

-- Query lenta: full table scan su 2 milioni di righe
SELECT * FROM orders WHERE user_id = 4321 AND status = 'pending';

-- EXPLAIN ANALYZE prima dell'indice: Seq Scan, ~850ms
-- Dopo l'indice composito:
CREATE INDEX idx_orders_user_status ON orders (user_id, status);
-- EXPLAIN ANALYZE dopo: Index Scan, ~2ms
-- Indice parziale: solo sulle righe che interrogherai davvero
CREATE INDEX idx_orders_pending ON orders (created_at)
WHERE status = 'pending';

Erreur courante : Créez un index à une seule colonne lorsque les requêtes filtrent toujours sur deux ou trois colonnes ensemble — un index composite (user_id, status) sert également des requêtes uniquement sur user_id, mais pas sur le ci-contre.

N+1 : Le bug de performances le plus courant avec ORM

// ❌ N+1: una query per ogni ordine per caricare l'utente
const orders = await this.orderRepo.find();
for (const order of orders) {
  order.user = await this.userRepo.findOne({ where: { id: order.userId } });
}

// ✅ Una singola query con join
const orders = await this.orderRepo.find({ relations: ['user'] });

// ✅ Prisma equivalente
const orders = await prisma.order.findMany({ include: { user: true } });

Sur 100 commandes, la version N+1 effectue 101 allers-retours vers la base de données. Avec une latence réseau aussi faible que 2 ms par requête, cela représente une perte de 200 ms qu'un seul JOIN élimine complètement.

Pagination : basée sur le curseur au-delà du décalage

// ❌ OFFSET su tabelle grandi: Postgres deve comunque scansionare e scartare le righe saltate
async findPage(page: number, limit = 20) {
  return this.repo.find({ skip: (page - 1) * limit, take: limit });
}

// ✅ Cursor-based: usa l'ultimo ID visto, sempre O(log n) con l'indice
async findAfter(cursorId: string | null, limit = 20) {
  return this.repo.find({
    where: cursorId ? { id: MoreThan(cursorId) } : {},
    order: { id: 'ASC' },
    take: limit,
  });
}

OFFSET 100000 force Postgres à compter et à exclure 100 000 lignes avant de renvoyer le résultat — sur les tables contenant des millions d'enregistrements, la pagination basée sur le curseur est plusieurs fois plus rapide et constitue la norme pour les flux infinis et les API publiques.

Projet réel : tableau de bord avec cache Redis

Un exemple minimal mais complet de bout en bout : un tableau de bord des commandes affichant des statistiques globales, mises en cache dans Redis et invalidées lors des événements d'écriture.

// orders-stats.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import type { Cache } from 'cache-manager';
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class OrdersStatsService {
  constructor(
    private prisma: PrismaService,
    @Inject(CACHE_MANAGER) private cache: Cache,
  ) {}

  async getDailyStats(date: string) {
    const cacheKey = `stats:daily:${date}`;
    const cached = await this.cache.get(cacheKey);
    if (cached) return cached;

    const stats = await this.prisma.order.groupBy({
      by: ['status'],
      where: { createdAt: { gte: new Date(`${date}T00:00:00Z`), lt: new Date(`${date}T23:59:59Z`) } },
      _count: true,
      _sum: { total: true },
    });

    await this.cache.set(cacheKey, stats, 5 * 60_000); // 5 minuti
    return stats;
  }

  async invalidateToday() {
    const today = new Date().toISOString().slice(0, 10);
    await this.cache.del(`stats:daily:${today}`);
  }
}
// orders.controller.ts
@Post()
async create(@Body() dto: CreateOrderDto) {
  const order = await this.ordersService.create(dto);
  await this.statsService.invalidateToday(); // invalida solo la chiave impattata
  return order;
}

Côté angulaire, le tableau de bord consomme ce point de terminaison avec resource() (Angular 19+), qui gère l'état de chargement/erreur sans passe-partout manuel :

import { resource } from '@angular/core';

@Component({ /* ... */ })
export class DashboardComponent {
  private http = inject(HttpClient);
  date = signal(new Date().toISOString().slice(0, 10));

  stats = resource({
    request: () => ({ date: this.date() }),
    loader: ({ request }) =>
      firstValueFrom(this.http.get<Stats[]>(`/api/orders/stats/${request.date}`)),
  });
}
@if (stats.isLoading()) {
  <app-spinner />
} @else if (stats.error()) {
  <app-error [message]="stats.error()" />
} @else {
  <app-stats-chart [data]="stats.value()" />
}

Journalisation et surveillance minimales mais correctes pour ce service :

// logger.interceptor.ts
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private logger = new Logger('HTTP');

  intercept(ctx: ExecutionContext, next: CallHandler) {
    const req = ctx.switchToHttp().getRequest();
    const start = Date.now();
    return next.handle().pipe(
      tap(() => this.logger.log(`${req.method} ${req.url} ${Date.now() - start}ms`)),
    );
  }
}

En production, cela devrait être remplacé par une journalisation structurée (pine) et des métriques exportées vers Prometheus/Grafana, mais le principe — mesurer la durée de chaque requête — est le même.

Optimisations avancées

Technique Quand l'utiliser Quand l’éviter
CDN pour les actifs statiques Toujours, pour les images/JS/CSS À ne jamais éviter en production
Limitation de débit API publiques, points de terminaison d'authentification Critères de jugement internes de faible exposition
Cache Redis Lectures fréquentes sur des données moins volatiles Données qui changent à chaque demande
Fils de travail Tâches liées au processeur (hachage, images) lié aux E/S (déjà géré par async/await)
Clusters / Répliques multiples Trafic saturant un seul cœur Application à faible trafic, surcharge inutile
Équilibreur de charge Plusieurs instances backend Instance unique, aucun avantage
Réplique en lecture de la base de données Lectures >> écrits, reportages abondants Petits ensembles de données, cohérence stricte requise
Mise à l'échelle horizontale Trafic variable, pointes saisonnières Goutts d'étranglement non résolus dans la base de données (échelle symptomatique uniquement)

Limitation de débit avec @nestjs/throttler:

import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot([{ ttl: 60_000, limit: 100 }]),
  ],
  providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }],
})
export class AppModule {}
@Throttle({ default: { limit: 5, ttl: 60_000 } }) // più stretto sul login
@Post('login')
login(@Body() dto: LoginDto) { /* ... */ }

Équilibreur de charge (Nginx, en amont de plusieurs instances) :

upstream nestjs_backend {
  least_conn;
  server backend1:3000;
  server backend2:3000;
  server backend3:3000;
}

server {
  location /api {
    proxy_pass http://nestjs_backend;
    proxy_set_header Connection "";
  }
}

least_conn distribue les requêtes à l'instance avec moins de connexions actives, plus efficace qu'un simple round-robin lorsque les requêtes ont des durées très différentes.

Benchmark : avant et après

Nombres collectés avec autocannon (100 connexions simultanées, 30 secondes) sur un point de terminaison GET /api/products?page=1 avec environ 50. 000 lignes dans le tableau produits.

Métrique Avant (pas de cache, pas d'index, pagination décalée) Après (Redis + index + pagination du curseur)
Temps de réponse moyen 420ms 18 ms
p95 890ms 35ms
p99 1,450 ms 62ms
Requêtes/s 210 4.100
CPU back-end (moyenne) 78% 22%
Mémoire RSS 340 Mo 190 Mo
Requête vers la base de données pour la demande 1 (lent, ~400 ms) ~0,1 (le cache est atteint 90 % du temps)

Commande utilisée :

npx autocannon -c 100 -d 30 http://localhost:3000/api/products?page=1

Commentaire : Le plus grand saut vient de l'index composite et de la mise en cache, et non des micro-optimisations du code. Le passage d'Express à Fastify sur ce même point de terminaison a généré +15 % de requêtes/s supplémentaires, mesurable mais secondaire par rapport au travail sur la base de données et le cache.

Sur le frontend, même principe : l'activation de la détection de changement sans zone sur un tableau de bord de 300 composants a réduit les cycles de détection de changement mesurés avec Angular DevTools Profiler de ~1 200 à ~80 par interaction utilisateur, avec un Time to Interactive qui est passé de 3,1 s à 1,4 s après avoir également ajouté @defer sur les graphiques sous le plier.

Sécurité

Les performances et la sécurité partagent souvent les mêmes contre-mesures (limitation du débit, validation des entrées), mais doivent être abordées explicitement.

// main.ts — checklist minima di sicurezza per un'API NestJS in produzione
await app.register(helmet); // header di sicurezza (CSP, HSTS, X-Frame-Options)
app.enableCors({
  origin: ['https://tuodominio.it'], // mai '*' con credenziali
  credentials: true,
});
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));

JWT et jeton d'actualisation :

@Injectable()
export class AuthService {
  async login(user: User) {
    const accessToken = this.jwt.sign({ sub: user.id }, { expiresIn: '15m' });
    const refreshToken = this.jwt.sign({ sub: user.id, type: 'refresh' }, { expiresIn: '7d' });
    await this.storeRefreshTokenHash(user.id, refreshToken); // hashato, mai in chiaro
    return { accessToken, refreshToken };
  }
}

Le jeton d'accès de courte durée (15 minutes) limite la fenêtre de risque en cas de vol ; le jeton d'actualisation doit être enregistré haché côté serveur et révocable (la déconnexion doit réellement l'invalider, pas seulement côté client).

Assainissement et validation avec class-validator:

export class CreateProductDto {
  @IsString() @Length(3, 100)
  name: string;

  @IsNumber() @Min(0)
  price: number;

  @IsOptional() @IsUrl()
  imageUrl?: string;
}

Journalisation sécurisée : n'enregistrez jamais de mots de passe, de jetons ou de données sensibles, même en cas d'erreur.

// ❌
this.logger.error(`Login fallito per ${dto.email} con password ${dto.password}`);

// ✅
this.logger.error(`Login fallito per utente ${dto.email}`);

Gestion centralisée des exceptions :

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse();
    const status = exception instanceof HttpException ? exception.getStatus() : 500;
    res.status(status).send({
      statusCode: status,
      message: exception instanceof HttpException ? exception.message : 'Errore interno',
      // niente stack trace nella risposta in produzione
    });
  }
}

Bonnes pratiques architecturales

Modèles de référentiel et injection de dépendances (natif dans NestJS) : séparez la logique de domaine de l'accès aux données, rendant les services testables sans véritable base de données.

@Injectable()
export class ProductsService {
  constructor(private readonly repo: ProductsRepository) {} // interfaccia, non implementazione diretta

  findFeatured() {
    return this.repo.findFeatured();
  }
}

Modularisation : chaque fonctionnalité NestJS en tant que module indépendant (ProductsModule, OrdersModule) avec des limites claires ; dans Angular, propose des composants autonomes avec un chargement paresseux par limite.

Configuration par environnement:

// config/configuration.ts
export default () => ({
  port: parseInt(process.env.PORT ?? '3000', 10),
  database: { url: process.env.DATABASE_URL },
  redis: { host: process.env.REDIS_HOST ?? 'localhost' },
});
ConfigModule.forRoot({
  isGlobal: true,
  load: [configuration],
  validationSchema: Joi.object({
    DATABASE_URL: Joi.string().required(),
    PORT: Joi.number().default(3000),
  }),
});

La validation des variables d'environnement au démarrage (avec Joi ou Zod) empêche qu'une faute de frappe en production ne soit découverte au moment de l'exécution plutôt qu'au déploiement.

Observabilité : corréler les journaux, les métriques et les traces avec un request-id propagé de bout en bout (en-tête X-Request-Id), exporté vers une pile comme Grafana + Loki + Tempo ou équivalent géré.

20 erreurs courantes

# Erreur Cause Solution
1 Requêtes sans index sur des colonnes souvent filtrées Non EXPLIQUER ANALYSER pendant le développement Ajoutez des index ciblés, vérifiez avec EXPLAIN
2 Requête N+1 avec ORM Relations chargées en boucle relations/include pour les jointures explicites
3 Cache sans invalidation Long TTL "pour la sécurité" Invalidation explicite sur les événements d'écriture
4 *ngFor/@for sans clé de piste Copier-coller sans penser à la redistribution du DOM Toujours suivre l'article. identifiant
5 Ensemble ballonné angulaire Importation de bibliothèques entières Importations ciblées, arborescence, analyse de bundles
6 Blocage de la boucle d'événement Fonctions synchrones liées au CPU dans les requêtes HTTP Threads de travail ou files d'attente de tâches
7 Pool de connexions trop petit Valeur par défaut non vérifiée Taille sur les cœurs de processeur et les répliques
8 Piscine de connexions trop grande "Plus c'est mieux" sans calcul Respectez la limite maximale de Postgres, utilisez PgBouncer
9 Pas de compression HTTP Oublié lors de l'installation compression/@fastify/compress en production
10 Papagation OFFSET sur d'immenses tables Modèle le plus simple à mettre en œuvre Pagination basée sur le curseur
11 Validation désactivée pour "vitesse" Perception incorrecte du coût réel Mesurez avant de désactiver, utilisez Zod si la vitesse est nécessaire
12 Secrets dans le journal Journalisation non filtrée Logger avec rédaction automatique des champs sensibles
13 CORS ouvert à * avec informations d'identification Copié du tutoriel Liste blanche explicite d'origine
14 Aucune limitation de débit lors de la connexion Sous-estimation du risque de force brute @nestjs/throttler avec des limites strictes sur les points de terminaison sensibles
15 Fuite de mémoire des auditeurs non supprimée EventEmitter. on sans off once() si possible, nettoyage explicite
16 Cache en mémoire illimité Carte grandissant indéfiniment lru-cache avec explicite max
17 SSR accédant à la fenêtre/document Code non examiné pour l'environnement serveur isPlatformBrowser() avant l'API du navigateur uniquement
18 Pas d'indice composite, uniquement des célibataires Ajouté un à la fois sans véritable analyse de requête Indice composite sur l'ordre réel des filtres
19 Threads de travail pour les tâches liées aux E/S Confusion entre les liaisons CPU et E/S async/await pour les E/S, travailleur pour le processeur uniquement
20 Pas de délai d'attente pour les appels externes Confiance aveugle dans les services tiers Délais d'attente explicites + disjoncteur

FAQ

Fastify est-il vraiment plus rapide qu'Express dans NestJS ? Oui, généralement 15 à 30 % de requêtes/s en plus avec la même logique, grâce à un routeur et une analyse plus efficaces. Le profit réel dépend du temps que votre application passe dans le framework HTTP par rapport à votre code/base de données.

Dois-je toujours utiliser Redis pour la mise en cache ? Non. Sur les petites applications à faible trafic, un cache en mémoire (lru-cache) peut suffire et évite une infrastructure supplémentaire. Redis devient nécessaire lorsque vous disposez de plusieurs instances backend qui doivent partager le même cache.

Zoneless Angular est-il prêt pour la production en 2026 ? Oui pour les nouveaux projets ou avec des dépendances mises à jour. Vérifiez d'abord que les bibliothèques tierces essentielles à votre projet ne nécessitent pas zone.js en interne.

Quelle est la différence entre OFFSET et la pagination du curseur ? OFFSET saute N lignes sur chaque page (coût augmentant avec N) ; la pagination du curseur utilise la dernière valeur vue comme point de départ (coût constant, nécessite un tri stable).

Combien de threads de travail dois-je créer ? Pas plus que le nombre de cœurs de processeur disponibles (os.availableParallelism()) ; au-delà de ce nombre, vous n'obtenez aucun véritable parallélisme, seulement une surcharge de changement de contexte.

Comment choisir le bon TTL pour un cache ? En fonction de l'ampleur des modifications des données et de la mesure dans laquelle il est tolérable d'afficher des données légèrement anciennes. Données du catalogue : minutes. Données financières ou d'inventaire en temps réel : secondes ou invalidation explicite, jamais de durée de vie longue.

Dois-je utiliser TypeORM ou Prisma ? Prisma offre une saisie plus forte et un générateur de requêtes plus ergonomique ; TypeORM est plus mature sur les modèles avancés d'Active Record/Data Mapper. Pour les nouveaux projets en 2026, j'ai tendance à préférer Prisma pour l'expérience du développeur.

Le mode cluster de Node duplique-t-il également la mémoire ? Oui, chaque travailleur est un processus distinct avec son propre tas. Si votre application utilise 200 Mo par instance, 4 workers signifie environ 800 Mo au total : planifiez la mémoire de votre serveur en conséquence.

Quand une réplique en lecture Postgres en vaut-elle la peine ? Lorsque la charge de lecture (tableau de bord, rapports) commence à entrer en concurrence avec les écritures transactionnelles sur la même instance. Cela ne résout pas une base de données mal indexée, cela déplace simplement le problème.

Comment puis-je réellement profiler une API NestJS ? node --prof pour le profilage du processeur, Chrome DevTools pour les instantanés de tas, EXPLAIN ANALYZE pour les requêtes lentes, autocannon/k6 pour les tests de charge de bout en bout.

Le casque est-il suffisant pour la sécurité HTTP ? C'est un bon point de départ (en-tête CSP, HSTS, etc.) mais doit être intégré avec la validation des entrées, la limitation du débit, la gestion correcte du JWT et le HTTPS obligatoire.

La limitation de débit doit-elle être placée au niveau de Nginx ou de NestJS ? Idéalement les deux : Nginx pour une première défense grossière contre le trafic anormal, @nestjs/throttler pour une logique fine par point de terminaison et par utilisateur.

Quelle est l'importance réelle de la compression Brotli par rapport à gzip ? Sur les charges utiles JSON typiques, réduction de taille supplémentaire de 10 à 20 % par rapport à gzip, avec un processeur légèrement plus élevé en compression (la décompression côté client est comparable).

@defer remplace complètement le chargement paresseux des itinéraires ? Non, ils sont complémentaires : le chargement paresseux des routes réduit le paquet initial pour des sections entières de l'application, @defer réduit le coût de rendu de composants lourds individuels à l'intérieur d'une page déjà chargée.

Comment empêcher le cache Redis de devenir un point de défaillance unique ? Redis en mode Sentinel ou Cluster pour une haute disponibilité ; au niveau de l'application, la mise en cache doit être une optimisation et non une dépendance stricte : si Redis est en panne, l'application devrait toujours pouvoir fonctionner (plus lentement) à partir de la base de données.

Est-il préférable de mettre à l'échelle verticalement ou horizontalement ? La version verticale (plusieurs CPU/RAM sur une seule instance) est plus simple mais présente un plafond et un point de défaillance unique. L'échelle horizontale (plus de répliques) s'adapte mieux mais nécessite que l'application soit sans état et que la base de données ne devienne pas un goulot d'étranglement.

À quelle fréquence dois-je consulter les index des bases de données ? Chaque fois que les modèles de requêtes dominants changent (nouvelles fonctionnalités, nouveaux filtres dans le tableau de bord) et périodiquement avec pg_stat_user_indexes pour trouver les index jamais utilisés qui ralentissent inutilement les écritures.

Les signaux angulaires remplacent-ils RxJS ? Pas complètement : les signaux sont parfaits pour l'état synchrone local des composants ; RxJS reste le mieux adapté aux flux asynchrones complexes (anti-rebond, nouvelle tentative, combinaison de plusieurs flux). Modern Angular les fait interagir avec toSignal()/toObservable().

Quelle est l'erreur la plus coûteuse que je vois récurrente dans les projets NestJS ? Les requêtes N+1 ne sont pas détectées jusqu'à ce que l'ensemble de données de production dépasse l'ensemble de données de test : fonctionne correctement avec 50 lignes, expire avec 50 000.

Avez-vous toujours besoin d'un CDN, même pour un petit projet ? Oui pour les ressources statiques (JS, CSS, images) : le coût d'installation est faible (souvent gratuit) et le bénéfice global en matière de latence est immédiat, quelle que soit l'ampleur du projet.

Conclusion

La performance n'est pas une fonctionnalité qui s'ajoute à la fin : c'est la somme des choix effectués tout au long du développement, depuis l'index du tableau de droite jusqu'à la track oubliée dans un @for. Les points qui font vraiment la différence, par ordre d'impact typique :

  1. Database — des index corrects et un regroupement de connexions dimensionné résolvent la plupart des goulots d'étranglement du backend.
  2. Mise en cache ciblée — Redis sur des lectures fréquentes, avec invalidation explicite, pas de TTL aveugle.
  3. Détection efficace des changements — signaux et @defer sur Angular pour réduire les travaux de rendu inutiles.
  4. Ne bloquez pas la boucle d'événements — threads de travail pour le processeur, jamais pour les E/S.
  5. Mesurer avant d'optimiser — EXPLIQUER ANALYSER, profileur, repères réels, pas de suppositions.

La prochaine étape naturelle à explorer est l'observabilité de bout en bout (traçage distribué avec OpenTelemetry) et les stratégies de mise à l'échelle automatique sur Kubernetes, qui méritent un article séparé.

Si ce guide vous a été utile, partagez-le avec votre équipe, laissez un commentaire avec la technique qui a eu le plus grand impact sur votre projet et abonnez-vous à la newsletter pour ne pas manquer les prochains insights sur les performances et l'architecture full-stack.

💬 Notes des lecteurs

0 notes

Écrire une note

Partagez votre avis, une suggestion ou un compliment

Dernières notes

Aucune note pour le moment. Soyez le premier à commenter !