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

Performance mit Angular, Nestjs und Node.js: Der definitive leitfaden 2026

Eine langsame Full-Stack-Anwendung ist fast nie die Schuld eines „langsamen Frameworks“. Es ist fast immer die Summe von zehn kleinen Fehlentscheidungen: ein *ngFor ohne trackBy, eine Abfrage ohne Index, ein Endpunkt, der die Antwort nicht komprimiert, ein zufällig großer Verbindungspool. Für sich genommen wirken sie wie Details. Zusammengenommen machen sie den Unterschied zwischen einer App, die in 80 ms reagiert, und einer App aus, die unter Last 2 Sekunden benötigt.

Dieser Leitfaden sammelt die Techniken, die ich tatsächlich in der Produktion auf Angular + NestJS + Node.js + PostgreSQL + Redis-Stack verwende, mit vollständigem Code, realistischen Benchmark-Zahlen und den häufigsten Fehlern, die ich in den letzten Jahren gesehen (und gemacht) habe.

Index

  1. Warum Leistung wichtig ist
  2. So funktioniert der Anfrage-Antwort-Ablauf
  3. Voraussetzungen und Referenzstapel
  4. Winkel optimieren
  5. NestJS optimieren
  6. Node.js auf Laufzeitebene optimieren
  7. Datenbank: PostgreSQL, Indizes und Verbindungspooling
  8. Echtes Projekt: Dashboard mit Redis-Cache
  9. Erweiterte Optimierungen
  10. Benchmark: vorher und nachher
  11. Sicherheit
  12. Best Practices für die Architektur
  13. 20 häufige Fehler
  14. FAQ
  15. Fazit

Warum Leistung wichtig ist

Was ist das in der Praxis? Die Optimierung der Leistung eines Angular/NestJS/Node.js-Stacks bedeutet, drei Zahlen zu reduzieren: die vom Benutzer wahrgenommene Zeit (Core Web Vitals auf der Frontend-Seite), die Antwortzeit des Backends (p50/p95/p99) und die für die Bearbeitung jeder Anfrage verbrauchten Ressourcen (CPU, Speicher, Verbindungen). DB).

Warum es wichtig ist. Google verwendet Core Web Vitals im Ranking. Amazon hat gemessen, dass jede weitere 100 ms Latenz Conversion-Prozentpunkte kostet. Aber der konkreteste Grund, den ich jeden Tag sehe, ist ein anderer: Ein Backend, das nicht linear skaliert, zwingt einen dazu, Hardware zu kaufen, anstatt besseren Code zu schreiben – und ab einem bestimmten Punkt reicht die Hardware nicht mehr aus.

Wann anzuwenden. Nicht sofort. Die vorzeitige Optimierung eines Endpunkts, der zehnmal am Tag aufgerufen wird, ist Zeitverschwendung. Die Techniken in diesem Leitfaden sollten angewendet werden, wenn: Sie über echte Profiling-Daten (keine Vermutungen) verfügen, der Datenverkehr zunimmt oder ein bestimmter Endpunkt in den Protokollen als Engpass erscheint.

Vorteile. Kürzere Reaktionszeiten, geringere Infrastrukturkosten, bessere SEO, geringere Benutzerabwanderung, Fähigkeit, Verkehrsspitzen ohne Ausfallzeiten zu bewältigen.

Nachteile. Jede Optimierung hat Kosten: mehr Komplexität (zu entwertender Cache, zu orchestrierende Worker), mehr Fehleroberfläche, Entwicklungszeit. Schlecht verwaltetes Caching ist die häufigste Ursache dafür, dass Benutzern veraltete Daten angezeigt werden.

Häufige Fehler. Optimieren, ohne vorher zu messen; Techniken aus Blogs kopieren, ohne den Kompromiss zu verstehen; Ignorieren der Datenbank (die häufigste Ursache für die Langsamkeit des NestJS-Stacks), während Wochen mit Angular-Mikrooptimierungen verbracht werden.

Echte Anwendungsfälle. Ein E-Commerce, der auf der Produktseite von 3 Sekunden auf 400 ms ansteigt und die Konversionen um 15 % steigert; ein internes Dashboard, das bei 200 gleichzeitigen Benutzern eine Zeitüberschreitung aufweist, da der Postgres-Pool auf 5 Verbindungen eingestellt ist; eine öffentliche API, die den 10-fachen Datenverkehr verarbeitet, nachdem Redis vor den umfangreichsten Abfragen hinzugefügt wurde.

So funktioniert der Anfrage-Antwort-Ablauf

Vor der Optimierung benötigen Sie ein mentales Modell dafür, wo die Zeit tatsächlich in einem Angular → NestJS → Postgres/Redis-Stack verbracht wird.

┌─────────────┐     ┌──────────────┐     ┌─────────────┐     ┌────────────┐
│   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)

Sequenzdiagramm für eine typische Anfrage (z. B. 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

Jeder Pfeil in diesem Diagramm ist eine Stelle, an der Sie Millisekunden verlieren oder gewinnen können. Die folgenden Abschnitte greifen sie nacheinander an.

Voraussetzungen und Referenzstapel

Um den Beispielen in dieser Anleitung zu folgen, benötigen Sie:

Werkzeug Empfohlene Version (2026) Notizen
Node.js 22 LTS oder höher Native Unterstützung für Worker-Threads und Fetch
Winkel 19+ Stabile Signale, optional zonenlos
NestJS 11+ Fastify-Unterstützung als alternativer Adapter zu Express
PostgreSQL 16+ Bessere Planerstatistiken, parallele Abfragen
Redis 7+ Unterstützung für granulare Funktionen und ACLs
Docker / Docker Compose letzter Stall Reproduzierbare lokale Umgebung
pnpm 9+ Schneller installieren als npm, festplatteneffizient
Prisma oder TypORM letzter Stall Typisiertes ORM

Man muss nicht vom ersten Tag an alles zusammen haben. Wenn Sie lesen, um ein bestehendes Projekt zu optimieren, sollte der Datenbankbereich (Indizes, Pooling) fast immer vor allem anderen angewendet werden: Hier verbirgt sich der größte Gewinn bei geringstem Risiko.

Schnelle Installation der Referenzumgebung

# 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 ersetzt Express als HTTP-Engine: Mit der gleichen Anwendungslogik verarbeitet Fastify dank eines effizienteren Routers und JSON-Parsing etwa 20–30 % mehr Anfragen pro Sekunde – es lohnt sich, es bei neuen Projekten einzusetzen.

Winkel optimieren

Zonenlose Änderungserkennung und Signale

Das historische Problem von Angular besteht darin, dass zone.js jedes asynchrone Ereignis (Klick, Timer, Abruf) abfängt und die Änderungserkennung im gesamten Komponentenbaum neu startet. Bei großen Anwendungen bedeutet dies Tausende nutzloser Steuerelemente für einen einzigen Klick.

Signale lösen das Problem an der Wurzel: Das Framework weiß genau welche Komponente von welchen Daten abhängt und aktualisiert nur diese.

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]);
  }
}

Warum es funktioniert: computed() führt nur dann eine Neuberechnung durch, wenn sich das Signal ändert, von dem es abhängt, und Angular aktualisiert nur die mit diesem Signal verknüpften DOM-Knoten, ohne die gesamte Komponente zu durchlaufen. In einer App mit klassischem zone.js hätte dasselbe addItem die Änderungserkennung für den gesamten Teilbaum der übergeordneten Komponente ausgelöst.

Bootstrap zonenlos (Angular 18+):

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

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

Vorteile: kleineres Bundle (zone.js wiegt ~30 KB), weniger Änderungserkennungszyklen, vorhersehbareres Debugging. Nachteile: Bibliotheken von Drittanbietern, die zone.js erwarten (einige ältere Versionen von Material- oder Grafikbibliotheken), erfordern möglicherweise manuelles NgZone.run(). Häufiger Fehler: Mischen von Signalen und veränderlicher Objektreferenz – wenn Sie ein Array mit .push() anstelle von .update() mutieren, erkennt das Signal die Änderung nicht.

trackBy und @for mit Schlüssel

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

Die neue Syntax @for erfordert einen track, im Gegensatz zum alten *ngFor wo trackBy war optional und wurde oft vergessen. Ohne einen stabilen Schlüssel zerstört Angular bei jeder Aktualisierung der Liste jeden DOM-Knoten und erstellt ihn neu, anstatt die vorhandenen neu anzuordnen – bei einer Tabelle mit 500 Zeilen ist das der Unterschied zwischen einer sofortigen Aktualisierung und einem sichtbaren Klick.

Erweitertes Lazy Loading und strategisches Vorladen

// 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 ist praktisch, aber naiv: Es lädt alles im Hintergrund herunter, auch wenn der Benutzer nie dort ankommt. Eine auf Ansichtsfenstern basierende Strategie (wie Quicklink, inspiriert von Google) lädt nur Formulare vor, die durch tatsächlich sichtbare Elemente verknüpft sind – reduziert die verschwendete Bandbreite auf Mobilgeräten.

SSR und inkrementelle Flüssigkeitszufuhr

// 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 />
}

SSR-Vorteile: Fast sofortiger erster Anstrich, ideal für SEO und Core Web Vitals (LCP). Nachteile: serverseitige CPU-Kosten für das Rendering, Code muss verwaltet werden, der sowohl auf dem Knoten als auch im Browser ausgeführt wird (nichts Fenster nicht angeschaut). @defer mit im Ansichtsfenster ist die effektivste Technik im Jahr 2026 für umfangreiche Komponenten (Diagramme, Rich-Text-Editoren, Karten), die für das erste Rendern nicht benötigt werden.

Bündelanalyse

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

Realistisches Ziel für eine Unternehmens-App: Erstpaket unter 150 KB im gzip-Format. Die häufigsten Ursachen für das Aufblähen von Bundles: Import ganzer Bibliotheken statt einzelner Funktionen (import _ from 'lodash' statt import debounce from 'lodash/debounce'), moment.js statt date-fns, SVG-Symbole, die als Komponenten statt Sprites importiert werden.

NestJS optimieren

HTTP-Komprimierungs- und Caching-Interceptor

// 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();

Brotli-Komprimierung (br) reduziert typische JSON-Nutzlasten um 70–80 % im Vergleich zu Standard-gzip, bei etwas höheren CPU-Kosten bei der Komprimierung – akzeptabel für die meisten REST-APIs.

Cache mit Redis und Cache-Manager

// 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);
  }
}

Was zwischengespeichert werden soll: öffentliche Antworten, viel häufiger gelesen als geschrieben (Produktkataloge, Blog-Artikellisten, Konfigurationen). Was auf der HTTP-Ebene niemals zwischengespeichert werden sollte: Antworten, die spezifische Daten für den authentifizierten Benutzer enthalten, ohne einen Cache-Schlüssel, der die Benutzer-ID enthält – ist der häufigste Sicherheitsfehler bei CacheInterceptor.

Explizite Ungültigmachung bei Datenänderung:

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;
}

Große Antwort-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());
}

Beim Exportieren Tausender Zeilen kann das Laden aller Daten mit find() in den Speicher und die anschließende Serialisierung in JSON den Knotenprozess-Heap auflösen. Durch das Streamen von Schreibvorgängen Zeile für Zeile bleibt die Speichernutzung unabhängig von der Datensatzgröße konstant.

Validierungsrohr: Achten Sie auf die Kosten

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

class-validator mit transform: true verwenden reflect-metadata und Reflection zur Laufzeit: auf Endpunkten, die Zehntausende Male pro Sekunde aufgerufen werden (z. B. interne Webhooks, Ereignisaufnahme). Validierungskosten können messbar werden. In diesen speziellen Fällen ist eine einfache manuelle Validierung (oder zod mit vorkompiliertem Schema) schneller.

Node.js auf Laufzeitebene optimieren

Ereignisschleife: Blockieren Sie sie niemals

// ❌ 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);
  });
}

Knoten. js ist Single-Threaded für JavaScript-Code: Eine teure synchrone Funktion (Hashing, Parsen großer Dateien, benutzerdefinierte Komprimierung) blockiert alle laufende Anforderungen, nicht nur die, die sie generiert hat. Arbeitsthreads verschieben CPU-gebundene Arbeit in separate Threads, sodass die Ereignisschleife für E/A frei bleibt.

Cluster-Modus: Alle Kerne verwenden

// 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
}

In der Produktion ziehe ich es vor, dies an PM2 (pm2 start dist/main.js -i max) oder Orchestrator (Kubernetes-Replikate) zu delegieren, anstatt Cluster manuell zu verwalten: gleicher Vorteil, weniger Code zu pflegen.

Speicherverwaltung: Lecks erkennen

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);
  }
}

Speicherlecks in Node entstehen fast immer durch: nicht entfernte Ereignis-Listener, In-Memory-Cache ohne Größenbeschränkung (verwenden Sie lru-cache mit einem max, niemals eine Map, die wächst auf unbestimmte Zeit), Abschluss, der Verweise auf große Objekte enthält, Timer (setInterval) nie gelöscht.

Worker-Threads für echte CPU-gebundene Arbeit

Entscheidungstabelle:

Szenario Lösung
Datenbankabfrage, externer HTTP-Aufruf async/await normal (E/A-gebunden, blockiert nicht)
Passwort-Hashing, PDF-Analyse, Bildverarbeitung Worker-Thread oder Job-Warteschlange (BullMQ)
Tausende kleine wiederholte Berechnungen Worker-Thread-Pool (z. B. Pool)
Lange Aufgaben (Minuten), nicht dringend Asynchrone Jobwarteschlange (Redis + BullMQ), Nicht-Worker-Thread inline

Datenbank: PostgreSQL, Indizes und Verbindungspooling

Das ist meiner Erfahrung nach der Abschnitt, der mehr wert ist als alle anderen zusammen. Die meisten Leistungsprobleme, die ich in der Produktion behoben habe, waren hier und nicht im Frontend.

Verbindungspooling behoben

// 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"

Faustregel: Verbindungslimit pro Backend-Instanz ≈ (Postgres-Server-CPU-Kerne × 2) / Anzahl der Backend-Replikate. Wenn mehrere NestJS-Replikate dasselbe Postgres teilen, verwenden Sie PgBouncer vor der Datenbank im Transaction-Pooling-Modus, um eine Erschöpfung der maximalen Postgres-Verbindungen (Standard 100) zu vermeiden.

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

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

Indizes: Die wirkungsvollste Einzeloptimierung

-- 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';

Häufiger Fehler: Erstellen Sie einen einspaltigen Index, wenn Abfragen immer nach zwei oder drei Spalten zusammen filtern – ein zusammengesetzter Index (Benutzer-ID, Status) bedient Abfragen auch nur nach Benutzer-ID, nicht jedoch nach gegenüber.

N+1: Der häufigste Leistungsfehler bei 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 } });

Von 100 Bestellungen führt Version N+1 101 Roundtrips zur Datenbank durch. Bei einer Netzwerklatenz von nur 2 ms pro Abfrage sind das verschwendete 200 ms, die durch einen einzigen JOIN vollständig eliminiert werden.

Paginierung: Cursorbasiert über den Versatz hinaus

// ❌ 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 zwingt Postgres, 100.000 Zeilen zu zählen und auszuschließen, bevor das Ergebnis zurückgegeben wird – bei Tabellen mit Millionen von Datensätzen ist die Cursor-basierte Paginierung um Größenordnungen schneller und der Standard für unendliche Feeds und öffentliche APIs.

Echtes Projekt: Dashboard mit Redis Cache

Ein minimales, aber vollständiges End-to-End-Beispiel: ein Bestell-Dashboard mit aggregierten Statistiken, die in Redis zwischengespeichert und bei Schreibereignissen ungültig gemacht werden.

// 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;
}

Angular-Seite: Das Dashboard verbraucht diesen Endpunkt mit resource() (Angular 19+), das den Lade-/Fehlerstatus ohne manuelles Boilerplate behandelt:

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()" />
}

Minimale, aber korrekte Protokollierung und Überwachung für diesen Dienst:

// 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`)),
    );
  }
}

In der Produktion sollte dies durch strukturierte Protokollierung (Pine) und den Export von Metriken nach Prometheus/Grafana ersetzt werden, aber das Prinzip – Messung der Dauer jeder Anfrage – ist dasselbe.

Erweiterte Optimierungen

Technik Wann man es verwendet Wann man es vermeiden sollte
CDN für statische Assets Immer, für Bilder/JS/CSS In der Produktion niemals zu vermeiden
Ratenbegrenzung Öffentliche APIs, Authentifizierungsendpunkte Interne Endpunkte mit geringer Exposition
Redis-Cache Häufiges Ablesen weniger volatiler Daten Daten, die sich mit jeder Anfrage ändern
Arbeitsthreads CPU-gebundene Aufgaben (Hashing, Bilder) E/A-gebunden (bereits von async/await verarbeitet)
Cluster/Mehrere Replikate Verkehr sättigt einen einzelnen Kern App mit geringem Datenverkehr, unnötiger Overhead
Lastausgleich Mehrere Backend-Instanzen Einzelinstanz, kein Nutzen
Datenbank-Lesereplikat Lesungen >> Schriften, umfangreiche Berichterstattung Kleine Datensätze, hohe Konsistenz erforderlich
Horizontale Skalierung Variabler Verkehr, saisonale Spitzen Ungelöste Datenbankengpässe (nur symptomatisch)

Ratenbegrenzung mit @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) { /* ... */ }

Load Balancer (Nginx, Upstream zu mehreren Instanzen):

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 verteilt Anfragen an die Instanz mit weniger aktiven Verbindungen, was effektiver als ein einfaches Round-Robin ist, wenn Anfragen sehr unterschiedliche Dauern haben.

Benchmark: vorher und nachher

Anzahl erfasst mit Autocannon (100 gleichzeitige Verbindungen, 30 Sekunden) auf einem Endpunkt GET /api/products?page=1 mit ca. 50. 000 Zeilen in der Tabelle Produkte.

Metrisch Vorher (kein Cache, kein Index, versetzte Paginierung) After (Redis + Index + Cursor-Paginierung)
Durchschnittliche Reaktionszeit 420ms 18ms
p95 890ms 35ms
p99 1.450ms 62ms
Anfragen/Sek. 210 4.100
Backend-CPU (Durchschnitt) 78% 22%
RSS-Speicher 340MB 190MB
Anfrage an DB für Anfrage 1 (langsam, ~400 ms) ~0,1 (Cache-Treffer in 90 % der Fälle)

Verwendeter Befehl:

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

Kommentar: Der größte Sprung kommt vom zusammengesetzten Index und Caching, nicht von Code-Mikrooptimierungen. Der Wechsel von Express zu Fastify auf demselben Endpunkt führte zu weiteren +15 % Anfragen/Sekunde, messbar, aber zweitrangig im Vergleich zur Arbeit an Datenbank und Cache.

Im Frontend gilt das gleiche Prinzip: Durch die Aktivierung der zonenlosen Änderungserkennung auf einem Dashboard mit 300 Komponenten wurden die mit Angular DevTools Profiler gemessenen Änderungserkennungszyklen von ~1.200 auf ~80 pro Benutzerinteraktion reduziert, wobei die Zeit bis zur Interaktion von 3,1 Sekunden auf 1,4 Sekunden sank, nachdem auch @defer in den Diagrammen darunter hinzugefügt wurde falten.

Sicherheit

Leistung und Sicherheit nutzen oft die gleichen Gegenmaßnahmen (Ratenbegrenzung, Eingabevalidierung), sollten aber explizit angesprochen werden.

// 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 und Aktualisierungstoken:

@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 };
  }
}

Kurzlebiges Zugriffstoken (15 Minuten) begrenzt das Risikofenster im Falle eines Diebstahls; Das Aktualisierungstoken muss auf der Serverseite gehasht gespeichert und widerrufbar sein (die Abmeldung muss es wirklich ungültig machen, nicht nur auf der Clientseite).

Bereinigung und Validierung mit Klassenvalidator:

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

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

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

Sichere Protokollierung: Protokollieren Sie niemals Passwörter, Token oder sensible Daten, auch nicht im Fehlerfall.

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

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

Zentralisierte Ausnahmebehandlung:

@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
    });
  }
}

Best Practices für die Architektur

Repository-Muster und Abhängigkeitsinjektion (nativ in NestJS): Trennen Sie die Domänenlogik vom Datenzugriff, sodass Dienste ohne eine echte Datenbank testbar sind.

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

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

Modularisierung: jede NestJS-Funktion als unabhängiges Modul (ProductsModule, OrdersModule) mit klaren Grenzen; In Angular stehen eigenständige Komponenten mit Lazy Loading pro Grenze zur Verfügung.

Konfiguration nach Umgebung:

// 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),
  }),
});

Die Validierung von Umgebungsvariablen beim Start (mit Joi oder Zod) verhindert, dass ein Tippfehler in der Produktion zur Laufzeit statt bei der Bereitstellung entdeckt wird.

Beobachtbarkeit: Korrelieren Sie Protokolle, Metriken und Traces mit einer durchgängig propagierten request-id (Header X-Request-Id), exportiert in einen Stack wie Grafana + Loki + Tempo oder verwaltetes Äquivalent.

20 häufige Fehler

zu
# Fehler Ursache Lösung
1 Abfragen ohne Index für häufig gefilterte Spalten Nein EXPLAIN ANALYZE während der Entwicklung Gezielte Indizes hinzufügen, prüfen Sie mit EXPLAIN
2 N+1 Abfrage mit ORM Schleifengeladene Beziehungen relations/include für explizite Verknüpfungen
3 Cache ohne Invalidierung Lange TTL „aus Sicherheitsgründen“ Explizite Invalidierung bei Schreibereignissen
4 *ngFür/@für ohne Gleisschlüssel Kopieren und Einfügen, ohne an DOM-Reflow zu denken Immer Artikel verfolgen. ID
5 Angular Bloated Bundle Import ganzer Bibliotheken Gezielte Importe, Tree-Shaking, Bundle-Analyse
6 Ereignisschleifenblockierung CPU-gebundene synchrone Funktionen in HTTP-Anfragen Worker-Threads oder Job-Warteschlangen
7 Verbindungspool zu klein Unüberprüfter Standardwert Größe auf CPU-Kernen und Replikaten
8 Verbindungspool zu groß „Mehr ist besser“ ohne Berechnung Respektieren Sie die Postgres-Höchstgrenze, verwenden Sie PgBouncer
9 Keine HTTP-Komprimierung Während der Einrichtung vergessen Komprimierung/@fastify/compress in Produktion
10 OFFSET-Paginierung auf großen Tabellen Am einfachsten zu implementierendes Muster Cursorbasierte Paginierung
11 Validierung für „Geschwindigkeit“ deaktiviert Falsche Wahrnehmung der tatsächlichen Kosten Vor dem Deaktivieren messen, Zod verwenden, wenn Geschwindigkeit erforderlich ist
12 Geheimnisse im Protokoll Ungefilterte Protokollierung Logger mit automatischer Schwärzung sensibler Felder
13 CORS offen für * mit Anmeldeinformationen Vom Tutorial kopiert Explizite Whitelist des Ursprungs
14 Keine Ratenbegrenzung beim Login Unterschätzung des Brute-Force-Risikos @nestjs/throttler mit engen Grenzen für sensible Endpunkte
15 Speicherleck von Listenern nicht entfernt EventEmitter. an ohne aus once() wo möglich, explizite Bereinigung
16 Unbegrenzter In-Memory-Cache Karte wächst auf unbestimmte Zeit lru-cache mit explizitem max
17 SSR greift auf Fenster/DokumentCode für Serverumgebung nicht geprüft isPlatformBrowser() vor der Nur-Browser-API
18 Kein zusammengesetzter Index, nur Singles Eins nach dem anderen ohne echte Abfrageanalyse hinzugefügt Zusammengesetzter Index zur tatsächlichen Reihenfolge der Filter
19 Worker-Threads für I/O-gebundene Aufgaben Verwechslung zwischen CPU-gebunden und E/A-gebunden async/await für I/O, Worker nur für CPU
20 Keine Zeitüberschreitung bei externen Anrufen Blindes Vertrauen in Dienste Dritter Explizite Zeitüberschreitungen + Leistungsschalter

FAQ

Ist Fastify in NestJS wirklich schneller als Express? Ja, im Allgemeinen 15–30 % mehr Anfragen/Sekunde bei gleicher Logik, dank eines effizienteren Routers und einer effizienteren Analyse. Der tatsächliche Gewinn hängt davon ab, wie viel Zeit Ihre App im HTTP-Framework im Vergleich zu Ihrem Code/Ihrer Datenbank verbringt.

Soll ich immer Redis zum Caching verwenden? Nein. Bei kleinen Anwendungen mit geringem Datenverkehr kann ein In-Memory-Cache (lru-cache) ausreichend sein und zusätzliche Infrastruktur vermeiden. Redis wird erforderlich, wenn Sie über mehrere Backend-Instanzen verfügen, die denselben Cache gemeinsam nutzen müssen.

Ist Zoneless Angular im Jahr 2026 produktionsbereit? Ja für neue Projekte oder mit aktualisierten Abhängigkeiten. Stellen Sie zunächst sicher, dass für Ihr Projekt wichtige Bibliotheken von Drittanbietern intern nicht zone.js erfordern.

Was ist der Unterschied zwischen OFFSET und Cursor-Paginierung? OFFSET überspringt N Zeilen auf jeder Seite (die Kosten steigen mit N); Die Cursor-Paginierung verwendet den zuletzt gesehenen Wert als Ausgangspunkt (konstante Kosten, erfordert stabile Sortierung).

Wie viele Arbeitsthreads soll ich erstellen? Nicht mehr als die Anzahl der verfügbaren CPU-Kerne (os.availableParallelism()); Über diese Zahl hinaus erhalten Sie keine wirkliche Parallelität, sondern nur den Kontextwechsel-Overhead.

Wie wähle ich die richtige TTL für einen Cache aus? Abhängig davon, wie stark sich die Daten ändern und wie stark es tolerierbar ist, leicht veraltete Daten anzuzeigen. Katalogdaten: Minuten. Finanz- oder Bestandsdaten in Echtzeit: Sekunden oder explizite Ungültigmachung, niemals lange TTL.

Soll ich TypeORM oder Prisma verwenden? Prisma bietet eine stärkere Eingabe und einen ergonomischeren Abfrage-Builder; TypeORM ist bei erweiterten Active Record/Data Mapper-Mustern ausgereifter. Für neue Projekte im Jahr 2026 bevorzuge ich aufgrund der Entwicklererfahrung tendenziell Prisma.

Wird im Cluster-Modus von Node auch Speicher dupliziert? Ja, jeder Worker ist ein separater Prozess mit seinem eigenen Heap. Wenn Ihre App 200 MB pro Instanz verwendet, bedeuten 4 Worker insgesamt ~800 MB – planen Sie Ihren Serverspeicher entsprechend.

Wann lohnt sich eine Postgres-Read-Replica? Wenn die Leselast (Dashboard, Berichte) beginnt, mit transaktionalen Schreibvorgängen auf derselben Instanz zu konkurrieren. Es behebt keine schlecht indizierte Datenbank, sondern verschiebt lediglich das Problem.

Wie profiliere ich eigentlich eine NestJS-API? node --prof für CPU-Profilerstellung, Chrome DevTools für Heap-Snapshots, EXPLAIN ANALYZE für langsame Abfragen, Autokanone/k6 für End-to-End-Lasttests.

Ist Helm ausreichend für HTTP-Sicherheit? Es ist ein guter Ausgangspunkt (CSP-Header, HSTS usw.), muss aber mit Eingabevalidierung, Ratenbegrenzung, korrekter Verwaltung von JWT und obligatorischem HTTPS integriert werden.

Sollte die Ratenbegrenzung auf Nginx- oder NestJS-Ebene erfolgen? Idealerweise beides: Nginx für einen ersten groben Schutz gegen anomalen Datenverkehr, @nestjs/throttler für feine Pro-Endpunkt- und Pro-Benutzer-Logik.

Wie wichtig ist die Brotli-Komprimierung wirklich im Vergleich zu gzip? Bei typischen JSON-Nutzlasten 10–20 % zusätzliche Größenreduzierung im Vergleich zu gzip, mit etwas höherer CPU bei der Komprimierung (clientseitige Dekomprimierung ist vergleichbar).

@defer ersetzt das verzögerte Laden von Routen vollständig? Nein, sie ergänzen sich: Lazy Loading von Routen reduziert das anfängliche Paket für ganze Abschnitte der App, @defer reduziert die Kosten für das Rendern einzelner schwerer Komponenten innerhalb einer bereits geladenen Seite.

Wie verhindere ich, dass der Redis-Cache zu einem Single Point of Failure wird? Redis im Sentinel- oder Cluster-Modus für hohe Verfügbarkeit; Auf Anwendungsebene sollte Caching eine Optimierung und keine harte Abhängigkeit sein – wenn Redis ausfällt, sollte die App immer noch (langsamer) aus der Datenbank bereitstellen können.

Ist es besser, vertikal oder horizontal zu skalieren? Vertikal (mehrere CPU/RAM auf einer einzelnen Instanz) ist einfacher, hat aber eine Obergrenze und einen einzelnen Fehlerpunkt. Horizontal (mehr Replikate) lässt sich besser skalieren, erfordert jedoch, dass die App zustandslos ist und die Datenbank nicht zum Engpass wird.

Wie oft sollte ich Datenbankindizes überprüfen? Immer wenn sich die vorherrschenden Abfragemuster ändern (neue Funktionen, neue Filter im Dashboard) und regelmäßig mit pg_stat_user_indexes, um nie verwendete Indizes zu finden, die Schreibvorgänge unnötig verlangsamen.

Ersetzen Winkelsignale RxJS? Nicht ganz: Signale eignen sich hervorragend für den synchronen Zustand lokaler Komponenten. RxJS eignet sich nach wie vor am besten für komplexe asynchrone Streams (Entprellen, erneutes Versuchen, Kombinieren mehrerer Streams). Modernes Angular ermöglicht die Interoperabilität mit toSignal()/toObservable().

Was ist der kostspieligste Fehler, der in NestJS-Projekten immer wieder auftritt? N+1-Abfragen werden erst erkannt, wenn der Produktionsdatensatz über den Testdatensatz hinaus wächst – funktioniert gut mit 50 Zeilen, Zeitüberschreitung bei 50.000,

Benötigen Sie auch für ein kleines Projekt immer ein CDN? Ja für statische Assets (JS, CSS, Bilder): Die Einrichtungskosten sind niedrig (oft kostenlos) und der allgemeine Latenzvorteil ist sofort spürbar, unabhängig von der Größe des Projekts.

Fazit

Leistung ist keine Funktion, die am Ende hinzugefügt wird: Sie ist die Summe der während der Entwicklung getroffenen Entscheidungen, vom Index in der rechten Tabelle bis zum Track, der in einem @für vergessen wurde. Die Punkte, die wirklich den Unterschied machen, in der Reihenfolge ihrer typischen Auswirkung:

  1. Datenbank – korrekte Indizes und Verbindungspooling in der richtigen Größe lösen die meisten Backend-Engpässe.
  2. Gezieltes Caching – Redis bei häufigen Lesevorgängen, mit expliziter Invalidierung, nicht blinder TTL.
  3. Effiziente Änderungserkennung – Signale und @defer auf Angular, um unnötige Rendering-Arbeit zu reduzieren.
  4. Ereignisschleife nicht blockieren – Arbeitsthreads für CPU-gebunden, niemals für E/A.
  5. Messen vor der Optimierung — EXPLAIN ANALYZE, Profiler, echte Benchmarks, keine Vermutungen.

Der nächste natürliche Schritt, den es zu erkunden gilt, ist End-to-End-Beobachtbarkeit (verteiltes Tracing mit OpenTelemetry) und automatische Skalierungsstrategien auf Kubernetes, die einen separaten Artikel verdienen.

Wenn dieser Leitfaden für Sie hilfreich war, teilen Sie ihn mit Ihrem Team, hinterlassen Sie einen Kommentar mit der Technik, die den größten Einfluss auf Ihr Projekt hatte, und abonnieren Sie den Newsletter, damit Sie die nächsten Einblicke in Leistung und Full-Stack-Architektur nicht verpassen.

💬 Leser-Notizen

0 Notizen

Notiz schreiben

Teile deine Meinung, einen Vorschlag oder ein Kompliment

Neueste Notizen

Noch keine Notizen. Sei der Erste, der kommentiert!