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

Performanca e Angular, Nestjs dhe Node.js: Udhëzuesi përfundimtar 2026

Një aplikacion i ngadalshëm me stack të plotë nuk është pothuajse asnjëherë faji i "një kornize të ngadaltë". Është pothuajse gjithmonë shuma e dhjetë vendimeve të vogla të këqija: një *ngFor pa trackBy, një pyetje pa indeks, një pikë përfundimtare që nuk e ngjesh përgjigjen, një grup lidhjesh me madhësi rastësore. Të marra individualisht duken si detaje. Së bashku, ato janë ndryshimi midis një aplikacioni që përgjigjet në 80 ms dhe atij që merr 2 sekonda nën ngarkesë.

Ky udhëzues mbledh teknikat që unë i përdor në prodhim në Angular + NestJS + Node.js + PostgreSQL + stack Redis, me kod të plotë, numra standarde realiste dhe gabimet më të zakonshme që kam parë (dhe bërë) gjatë viteve të fundit.

Indeksi

  1. Pse ka rëndësi performanca
  2. Si funksionon rrjedha e kërkesë-përgjigje
  3. Parakushtet dhe rafti i referencës
  4. Optimizo Angular
  5. Optimizo NestJS
  6. Optimizo Node.js në nivelin e ekzekutimit
  7. Baza e të dhënave: PostgreSQL, indekset dhe bashkimi i lidhjeve
  8. Projekt real: Pult me cache Redis
  9. Optimizime të avancuara
  10. Benchmark: para dhe pas
  11. Siguria
  12. Praktikat më të mira arkitekturore
  13. 20 gabime të zakonshme
  14. FAQ
  15. Përfundim

Pse ka rëndësi performanca

Çfarë është ajo, në praktikë. Optimizimi i performancës së një grumbulli Angular/NestJS/Node.js nënkupton reduktimin e tre numrave: kohën e perceptuar nga përdoruesi (Core Web Vitals në anën e përparme), kohën e përgjigjes së burimeve të pasme/p5p0995 shërbejë çdo kërkesë (CPU, memorie, lidhje DB).

Pse ka rëndësi. Google përdor Core Web Vitals në renditje. Amazon mati se çdo 100 ms shtesë vonese kushton pikë përqindjeje konvertimi. Por arsyeja më konkrete, ajo që shoh çdo ditë, është një tjetër: një backend që nuk shkallëzon në mënyrë lineare të detyron të blesh pajisje në vend që të shkruash kod më të mirë — dhe në një moment hardueri nuk është më i mjaftueshëm.

Kur duhet aplikuar. Jo menjëherë. Optimizimi i parakohshëm i një pike përfundimtare të quajtur 10 herë në ditë është humbje kohe. Teknikat në këtë udhëzues duhet të zbatohen kur: keni të dhëna reale të profilizimit (jo supozime), trafiku po rritet ose një pikë përfundimtare specifike shfaqet në regjistra si një pengesë.

Përfitimet. Koha më e ulët e përgjigjes, kostot e reduktuara të infrastrukturës, SEO më e mirë, frenimi më i ulët i përdoruesve, aftësia për të trajtuar maksimumin e trafikut pa ndërprerje.

Disavantazhet. Çdo optimizim ka një kosto: më shumë kompleksitet (cache për të zhvlerësuar, punëtorët për të orkestruar), më shumë sipërfaqe të gabimeve, kohë zhvillimi. Regjistrimi i keq i menaxhuar në memorie është shkaku numër një i të dhënave të vjetruara që u shfaqen përdoruesve.

Gabimet e zakonshme. Optimizo pa matur më parë; teknikat e kopjimit nga blogjet pa e kuptuar kompromisin; duke injoruar bazën e të dhënave (shkaku numër një i ngadalësisë në stivën e NestJS) ndërsa kaloni javë në mikro-optimizimet Angular.

Raste përdorimi real. Një tregti elektronike që shkon nga 3s në 400ms në faqen e produktit duke rritur konvertimet me 15%; një panel i brendshëm që përfundon me 200 përdorues të njëkohshëm sepse grupi i Postgres është vendosur në 5 lidhje; një API publike që trajton trafikun 10 herë pasi shton Redis përpara pyetjeve më të rënda.

Si funksionon rrjedha e kërkesë-përgjigje

Para optimizimit, ju nevojitet një model mendor se ku shpenzohet koha në të vërtetë në një pirg 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)

Diagrami i sekuencës për një kërkesë tipike (p.sh. 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

Çdo shigjetë në këtë diagram është një vend ku mund të humbni - ose të fitoni - milisekonda. Pjesët e mëposhtme i sulmojnë një nga një.

Parakushtet dhe rafti i referencës

Për të ndjekur shembujt në këtë udhëzues ju nevojiten:

Mjet Versioni i rekomanduar (2026) Shënime
Nyja.js 22 LTS ose më i lartë Mbështetje vendase për fijet e punëtorëve dhe fetch
Këndore 19+ Sinjale të qëndrueshme, opsionale pa zona
NestJS 11+ Fastify mbështetjen si një përshtatës alternativ për Express
PostgreSQL 16+ Statistika më të mira të planifikuesit, pyetje paralele
Redis 7+ Mbështetje për funksionet granulare dhe ACL
Docker / Docker Compose stabile e fundit Mjedis lokal i riprodhueshëm
pnpm 9+ Instaloni më shpejt se npm, me efikasitet në disk
Prism ose TypeORM stabile e fundit ORM i shtypur

Nuk keni nevojë për gjithçka së bashku që nga dita e parë. Nëse jeni duke lexuar për të optimizuar një projekt ekzistues, seksioni i bazës së të dhënave (indekset, bashkimi) duhet pothuajse gjithmonë të aplikohet para çdo gjëje tjetër: është vendi ku fshihet fitimi më i madh me rrezikun më të vogël.

Instalim i shpejtë i mjedisit të referencës

# 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 zëvendëson Express si motor HTTP: me të njëjtën logjikë aplikacioni, Fastify trajton rreth 20-30% më shumë kërkesa në sekondë falë një ruteri më efikas dhe analizës JSON — ia vlen të miratohet 4 projekt i ri.

Optimizo Angular

Zbulimi dhe sinjalet e ndryshimeve pa zonë

Problemi historik i Angular është se zone.js përgjon çdo ngjarje asinkrone (kliko, kohëmatës, marr) dhe rinis zbulimin e ndryshimit në të gjithë pemën përbërëse. Në aplikacione të mëdha kjo nënkupton mijëra kontrolle të padobishme për një klikim të vetëm.

Sinjalet e zgjidhin problemin në rrënjë: korniza e di saktësisht cili komponent varet nga cilat të dhëna dhe përditëson vetëm atë.

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

Pse funksionon: computed() rillogaritet vetëm kur sinjali varet nga ai ndryshon, dhe Angular përditëson vetëm nyjet DOM të lidhura me atë sinjal, pa e përshkuar të gjithë komponentin. Në një aplikacion me zone.js klasik, i njëjti addItem do të kishte shkaktuar zbulimin e ndryshimit në të gjithë nënpemën e komponentit mëmë.

Bootstrap pa zona (Angular 18+):

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

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

Avantazhet: paketë më e vogël (zone.js peshon ~30 KB), më pak cikle zbulimi të ndryshimeve, korrigjimi më i parashikueshëm. Kundër: Bibliotekat e palëve të treta që presin zone.js (disa versione më të vjetra të bibliotekave materiale ose grafike) mund të kërkojnë manual NgZone.run(). Gabim i zakonshëm: përzierja e sinjaleve dhe referenca e objektit të ndryshueshme — nëse ndryshoni një grup me .push() në vend të .update(), sinjali nuk e dallon ndryshimin.

trackBy dhe @for me çelës

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

Sintaksa e re @for kërkon një track, ndryshe nga e vjetra *ngFor ku trackBy ishte opsionale dhe shpesh harrohej. Pa një çelës të qëndrueshëm, Angular shkatërron dhe rikrijon çdo nyje DOM me çdo përditësim të listës në vend që të rirendit ato ekzistuese - në një tabelë me 500 rreshta është ndryshimi midis një përditësimi të menjëhershëm dhe një klikimi të dukshëm.

Ngarkim i avancuar dembel dhe parangarkim strategjik

// 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 është i përshtatshëm, por naiv: shkarkon gjithçka në sfond edhe nëse përdoruesi nuk do të arrijë kurrë atje. Një strategji e bazuar në portat e shikimit (si lidhja e shpejtë, e frymëzuar nga Google) ngarkon paraprakisht vetëm format e lidhura nga elementë të dukshëm - redukton gjerësinë e brezit të humbur në celular.

SSR dhe hidratim në rritje

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

Përfitimet SSR: Boja e parë pothuajse e menjëhershme, e shkëlqyeshme për SEO dhe Core Web Vitals (LCP). Kundërt: kostoja e CPU-së nga ana e serverit për paraqitje, duhet të menaxhoni kodin që funksionon si në Node ashtu edhe në shfletues (asgjë dritare nuk shikohet). @defer me në portin e pamjes është teknika më efektive në vitin 2026 për komponentët e rëndë (grafikë, redaktues me tekst të pasur, harta) që nuk nevojiten për paraqitjen e parë.

Analiza e paketës

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

Objektivi real për një aplikacion ndërmarrje: paketa fillestare nën 150 KB gzip. Shkaqet më të zakonshme të fryrjes së paketave: importimi i bibliotekave të tëra në vend të funksioneve individuale (import _ nga 'lodash' në vend të importo debounce nga 'lodash/debounce'), moment.js në vend të ikonave të data-f.

Optimizo NestJS

Përgjimi i kompresimit dhe ruajtjes së 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();

Kompresimi Brotli (br) redukton ngarkesat tipike JSON me 70-80% krahasuar me gzip standard, me një kosto pak më të lartë të CPU-së në kompresim — e pranueshme për shumicën e API-ve REST.

Cache me Redis dhe menaxherin e 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);
  }
}

Çfarë të ruhet në memorie: përgjigjet publike, lexohen shumë më shpesh sesa janë shkruar (katalogët e produkteve, listat e artikujve në blog, konfigurimet). Çfarë nuk duhet të ruhet kurrë në memorie në shtresën HTTP: Përgjigjet që përmbajnë të dhëna specifike për përdoruesin e vërtetuar pa një çelës memorie që përfshin ID-në e përdoruesit — është gabimi më i zakonshëm i sigurisë me CacheInterceptor.

Pavlefshmëria e qartë kur të dhënat ndryshojnë:

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

Përgjigje të mëdha në transmetim

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

Për eksportimin e mijëra rreshtave, ngarkimi i gjithçkaje në memorie me find() dhe më pas serializimi në JSON mund të shpërthejë grumbullin e procesit Node. Transmetimi shkruan rresht pas rreshti duke e mbajtur konstant përdorimin e kujtesës, pavarësisht nga madhësia e të dhënave.

Tubacioni i verifikimit: kushtojini vëmendje kostos

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

class-validator me transform: true përdor reflekt-metadata dhe reflektim në kohën e ekzekutimit: dhjetëra herë të brendshme të thirrura në pikën fundore.g. webhooks, gëlltitje ngjarjesh) kostoja e vërtetimit mund të bëhet e matshme. Në ato raste specifike, një vërtetim manual i lehtë (ose zod me skemë të parapërpiluar) është më i shpejtë.

Optimizo Node.js në nivelin e ekzekutimit

Cak i ngjarjes: mos e bllokoni kurrë atë

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

Nyja. js është me një fillesë të vetme për kodin JavaScript: një funksion sinkron i shtrenjtë (hashimi, analizimi i madh i skedarëve, kompresimi i personalizuar) bllokon të gjitha kërkesat në progres, jo vetëm atë që e ka krijuar atë. Fijet e punës lëvizin punën e lidhur me CPU-në në fije të veçanta, duke e lënë qarkun e ngjarjeve të lirë për të shërbyer I/O.

Modaliteti Cluster: përdor të gjitha bërthamat

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

Në produksion preferoj t'ia delegoj këtë PM2 (pm2 start dist/main.js -i max) ose orkestratorit (kopjet e Kubernetes) në vend që të menaxhoj cluster me dorë: përfitim i njëjtë, më pak kod për mirëmbajtje.

Menaxhimi i memories: zbuloni rrjedhjet

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

Rrjedhjet e memories në Node lindin pothuajse gjithmonë nga: dëgjuesit e ngjarjeve të pa hequra, cache në memorie pa kufi të madhësisë (përdorni lru-cache me një max, kurrë një Map që rritet pafundësisht), mbyllja që mban referenca për objekte të mëdha, kohëmatësit (setInterval) nuk u fshinë kurrë.

Fijet e punës për punë reale të lidhura me CPU

Tabela e Vendimeve:

Skenari Zgjidhje
Kërkesa e bazës së të dhënave, telefonata e jashtme HTTP asinkronizuar/prit normal (I/O-lidhur, nuk bllokohet)
Hashimi i fjalëkalimit, analizimi i PDF-së, përpunimi i imazhit Fije punëtore ose radhë pune (BullMQ)
Mijëra llogaritje të vogla të përsëritura Pishinë me fije pune (p.sh. pishinë)
Detyra të gjata (minuta), jo urgjente Radhë pune asinkrone (Redis + BullMQ), fill jo-punëtor në linjë

Baza e të dhënave: PostgreSQL, indekset dhe bashkimi i lidhjeve

Ky është, sipas përvojës sime, seksioni që vlen më shumë se gjithë të tjerët së bashku. Shumica e problemeve të performancës që rregullova në prodhim ishin këtu, jo në front.

Bashkimi i lidhjes fikse

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

Rregulli i parë: lidhja_limit për shembull backend ≈ (Bërthama CPU e serverit Postgres × 2) / numri i kopjeve të backend-it. Me kopje të shumta të NestJS që ndajnë të njëjtin Postgres, përdorni PgBouncer përpara bazës së të dhënave në modalitetin e bashkimit transaction për të shmangur rraskapitjen e lidhjeve maksimale Postgres (parazgjedhja 100).

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

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

Indekset: Optimizimi i vetëm më me ndikim

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

Gabim i zakonshëm: Krijoni një indeks me një kolonë kur pyetjet filtrohen gjithmonë në dy ose tre kolona së bashku — një indeks i përbërë (id_user, status) shërben gjithashtu vetëm në pyetje user_id, por jo e kundërta.

N+1: Defekti më i zakonshëm i performancës me 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 } });

Nga 100 porosi, versioni N+1 kryen 101 vajtje vajtje-ardhje në bazën e të dhënave. Me vonesë të rrjetit deri në 2 ms për pyetje, këto janë 200 ms të humbura që një JOIN e eliminon plotësisht.

Faqimi: i bazuar në kursor përtej kompensimit

// ❌ 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 detyron Postgres të numërojë dhe të përjashtojë 100,000 rreshta përpara se të kthejë rezultatin - në tabelat me miliona regjistrime, faqezimi i bazuar në kursor është urdhra të madhësisë dhe është standardi më i shpejtë për publikun në PI dhe është më i shpejtë.

Projekti i vërtetë: Pult me Cache Redis

Një shembull minimal, por i plotë nga fundi në fund: një panel kontrolli porosish që tregon statistika të përgjithshme, të ruajtura në memorie të fshehtë në Redis dhe të pavlefshme në ngjarjet e shkrimit.

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

Anë këndore, pulti e konsumon këtë pikë fundore me burim() (Angular 19+), i cili trajton gjendjen e ngarkimit/gabimit pa bojler manual:

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

Regjistrimi dhe monitorimi minimal por korrekt për këtë shërbim:

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

Në prodhim kjo duhet të zëvendësohet me prerje të strukturuar (pishë) dhe metrikë të eksportuar në Prometheus/Grafana, por parimi — matja e kohëzgjatjes së çdo kërkese — është i njëjtë.

Optimizime të avancuara

Teknika Kur ta përdorni Kur duhet shmangur
CDN për aktivet statike Gjithmonë, për imazhe/JS/CSS Nuk duhet shmangur kurrë në prodhim
Kufizimi i normës API-të publike, pikat e fundit të vërtetimit Pika të brendshme me ekspozim të ulët
Redis cache Lexime të shpeshta në të dhëna më pak të paqëndrueshme Të dhënat që ndryshojnë me çdo kërkesë
Fijet punëtore Detyrat e lidhura me CPU (hashing, imazhe) I/O-lidhur (i trajtuar tashmë nga async/prit)
Grupe / kopje të shumëfishta Trafiku që ngopet me një bërthamë Aplikacion me trafik të ulët, shpenzime të panevojshme
balancues ngarkese Instanca të shumëfishta backend Instancë e vetme, pa përfitim
Replika e leximit të bazës së të dhënave Lexime >> shkrime, raportime te renda Të dhëna të vogla, kërkohet qëndrueshmëri e ngushtë
Shkallëzimi horizontal Trafik i ndryshueshëm, maja sezonale Grykat e pazgjidhura të bazës së të dhënave (vetëm në shkallë simptomatike)

Kufizimi i normës me @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) { /* ... */ }

balancuesi i ngarkesës (Nginx, në rrjedhën e sipërme në disa raste):

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 shpërndan kërkesa në instancën me më pak lidhje aktive, më efektive se sa një rrotullim i thjeshtë kur kërkesat kanë kohëzgjatje shumë të ndryshme.

Benchmark: para dhe pas

Numrat e mbledhur me autocannon (100 lidhje të njëkohshme, 30 sekonda) në një pikë fundore GET /api/products?page=1 me afërsisht 50. 000 rreshta në tabelë produkte.

Metrikë Përpara (pa memorie të fshehtë, pa indeks, faqezim të kompensuar) Pas (Redis + indeks + faqezim i kursorit)
Koha mesatare e përgjigjes 420ms 18ms
p95 890ms 35ms
p99 1.450ms 62ms
Kërkesa/sek 210 4.100
CPU Backend (mesatar) 78% 22%
Memorie RSS 340MB 190MB
Pyetje në DB për kërkesë 1 (i ngadalshëm, ~400ms) ~0.1 (cache goditi 90% të rasteve)

Komanda e përdorur:

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

Koment: Kërcimi më i madh vjen nga indeksi i përbërë dhe ruajtja memorike, jo nga mikro-optimizimet e kodit. Kalimi nga Express në Fastify në të njëjtën pikë përfundimtare dha një shtesë +15% kërkesa/sek, të matshme por dytësore në krahasim me punën në bazën e të dhënave dhe cache.

Në pjesën e përparme, i njëjti parim: aktivizimi i zbulimit të ndryshimeve pa zona në një panel me 300 komponentë reduktoi ciklet e zbulimit të ndryshimeve të matura me Angular DevTools Profiler nga ~1200 në ~80 për ndërveprim të përdoruesit, me një kohë për ndërveprim që ra nga 141 gjithashtu. @defer në grafikët poshtë palosjes.

Siguria

Performanca dhe siguria shpesh ndajnë të njëjtat kundërmasa (kufizimi i normës, vërtetimi i hyrjes), por duhet të adresohen në mënyrë eksplicite.

// 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 dhe token refresh:

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

Tokeni i aksesit jetëshkurtër (15 minuta) kufizon dritaren e rrezikut në rast vjedhjeje; token-i i rifreskimit duhet të ruhet i hashuar në anën e serverit dhe të revokohet (dalja duhet ta zhvlerësojë atë, jo vetëm në anën e klientit).

Sanitizimi dhe vërtetimi me class-validator:

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

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

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

Regjistrim i sigurt: kurrë mos regjistroni fjalëkalime, argumente ose të dhëna të ndjeshme edhe në rast gabimi.

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

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

Trajtimi i centralizuar i përjashtimeve:

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

Praktikat më të mira arkitekturore

Modelet e depove dhe Injeksioni i varësisë (vendas në NestJS): Ndani logjikën e domenit nga aksesi i të dhënave, duke i bërë shërbimet të testueshme pa një bazë të dhënash reale.

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

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

Modularizimi: çdo veçori e NestJS si një modul i pavarur (Moduli i produkteve, Moduli i porosive bounde); në Angular, shfaq komponentë të pavarur me ngarkim dembel për çdo kufi.

Konfigurimi sipas mjedisit:

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

Verifikimi i variablave të mjedisit në fillim (me Joi ose Zod) parandalon zbulimin e një gabimi shtypi në prodhim në kohën e ekzekutimit në vend që të vendoset.

Vëzhgueshmëria: ndërlidh regjistrat, metrikat dhe gjurmët me një staf të përhapur nga fundi në fund kërkesa-id (header X-Kërkesë2-Kërkesë) Grafana + Loki + Tempo ose ekuivalent i menaxhuar.

20 gabime të zakonshme

# Gabim Shkaku Zgjidhje
1 Pyetje pa indeks në kolonat e filtruara shpesh Jo EXPLAIN ANALYZE gjatë zhvillimit Shto indekse të synuara, kontrollo me EXPLAIN
2 N+1 pyetje me ORM Marrëdhëniet e ngarkuara me lak relacione/përfshi për bashkime të qarta
3 Cache pa zhvlerësim TTL e gjatë "për siguri" Pavlefshmëria e qartë në ngjarjet e shkrimit
4 *ngFor/@for pa çelës pista Copy-paste pa menduar për reflow DOM Gjithmonë gjurmo artikullin. id
5 Bundle e fryrë këndore Import i të gjitha bibliotekave Importet e synuara, tundja e pemëve, analiza e paketave
6 Bllokimi i ciklit të ngjarjeve Funksionet sinkrone të lidhura me CPU në kërkesat HTTP Fijet e punëtorëve ose radhët e punës
7 Pishina e lidhjes shumë e vogël Vlera e paracaktuar e pashqyrtuar Madhësia në bërthamat dhe kopjet e CPU-së
8 Pishina e lidhjes shumë e madhe "Më shumë është më mirë" pa llogaritje Respektoni kufirin maksimal të Postgres, përdorni PgBouncer
9 Nuk ka kompresim HTTP Harruar gjatë konfigurimit kompresim/@fastify/compress në prodhim
10 Faqtimi OFFSET në tavolina të mëdha Modeli më i lehtë për t'u zbatuar Faqizimi i bazuar në kursor
11 Verifikimi i çaktivizuar për "shpejtësinë" Perceptim i gabuar i kostos reale Mas përpara se ta çaktivizosh, përdor Zod nëse nevojitet shpejtësi
12 Sekretet në log Regjistrim i pafiltruar Loger me redaktimin automatik të fushave të ndjeshme
13 CORS hapur në * me kredencialet Kopjuar nga tutoriali Lista e bardhë e qartë e origjinës
14 Nuk ka kufizim tarifash në hyrje Nënvlerësimi i rrezikut të forcës brutale @nestjs/throttler me kufij të ngushtë në pikat përfundimtare të ndjeshme
15 Rrjedhja e kujtesës nga dëgjuesit nuk është hequr Event Emitter. në pa off njëherë() ku është e mundur, pastrim i qartë
16 Cache e pakufizuar në memorie Harta në rritje pafundësisht lru-cache me max
17 SSR duke hyrë në dritare/dokument Kodi nuk është shikuar për mjedisin e serverit isPlatformBrowser() përpara API-së vetëm për shfletuesin
18 Nuk ka indeks të përbërë, vetëm teke Shtuar një nga një pa analizë reale të pyetjeve Indeksi i përbërë në rendin aktual të filtrave
19 Fijet e punës për detyrat e lidhura me I/O Konfuzion midis CPU-së dhe I/O-lidhur async/wait për I/O, punëtor vetëm për CPU
20 Nuk ka kohë për thirrjet e jashtme Besimi i verbër në shërbimet e palëve të treta Përfundime eksplicite + ndërprerës qarku

FAQ

A është vërtet Fastify më i shpejtë se Express në NestJS? Po, përgjithësisht 15-30% më shumë kërkesa/sek me të njëjtën logjikë, falë një ruteri dhe analizimi më efikas. Fitimi aktual varet nga sa kohë shpenzon aplikacioni juaj në kornizën HTTP kundrejt kodit/bazës së të dhënave.

A duhet të përdor gjithmonë Redis për memorie? Jo. Në aplikacionet e vogla me trafik të ulët, një cache në memorie (lru-cache) mund të jetë e mjaftueshme dhe shmang infrastrukturën shtesë. Redis bëhet i domosdoshëm kur keni shumë raste të backend-it që duhet të ndajnë të njëjtën cache.

A është Zoneless Angular gati për prodhim në vitin 2026? Po për projekte të reja ose me varësi të përditësuara. Së pari verifikoni që bibliotekat e palëve të treta kritike për projektin tuaj nuk kërkojnë zone.js brenda vendit.

Cili është ndryshimi midis OFFSET dhe faqosjes së kursorit? OFFSET anashkalon N rreshta në secilën faqe (kosto rritet me N); faqëzimi i kursorit përdor vlerën e parë të fundit si pikënisje (kosto konstante, kërkon renditje të qëndrueshme).

Sa fije pune duhet të krijoj? Jo më shumë se numri i bërthamave të CPU-së në dispozicion (os.availableParallelism()); përtej këtij numri, ju nuk fitoni paralelizëm të vërtetë, por vetëm ndërrimi i kontekstit.

Si mund të zgjedh TTL-në e duhur për një cache? Varësisht se sa ndryshojnë të dhënat dhe sa është e tolerueshme të tregohet një e dhënë paksa e vjetër. Të dhënat e katalogut: minuta. Të dhënat financiare ose të inventarit në kohë reale: sekonda ose anulim i qartë, asnjëherë TTL e gjatë.

A duhet të përdor TypeORM apo Prisma? Prisma ofron shtypje më të fortë dhe ndërtues më ergonomik të pyetjeve; TypeORM është më i pjekur në modelet e avancuara të regjistrimit aktiv/hartës të të dhënave. Për projektet e reja në 2026 prirem të preferoj Prisma për përvojën e zhvilluesit.

Modaliteti i grupimit të Node-it gjithashtu dublikon kujtesën? Po, çdo punëtor është një proces më vete me grumbullin e vet. Nëse aplikacioni juaj përdor 200 MB për shembull, 4 punëtorë do të thotë ~800 MB gjithsej - planifikoni kujtesën e serverit tuaj në përputhje me rrethanat.

Kur ia vlen një kopje e lexuar e Postgres? Kur ngarkesa e leximit (pulti, raportet) fillon të konkurrojë me shkrimet transaksionale në të njëjtin shembull. Nuk rregullon një bazë të dhënash të indeksuar dobët, thjesht e zhvendos problemin.

Si mund të profilizoj në të vërtetë një API të NestJS? node --prof për profilizimin e CPU, Chrome DevTools për fotografitë e grumbullit, EXPLAIN ANALYZE për pyetje të ngadalta, autocannon/k6 për testimin e ngarkesës nga fundi në fund.

A mjafton helmeta për sigurinë HTTP? Është një pikënisje e mirë (titulli CSP, HSTS, etj.) por duhet të integrohet me vlefshmërinë e hyrjes, kufizimin e normës, menaxhimin e saktë të JWT dhe HTTPS të detyrueshëm.

A duhet vendosur kufizimi i tarifave në nivelin Nginx ose NestJS? Idealisht të dyja: Nginx për një mbrojtje të parë të papërpunuar kundër trafikut anormal, @nestjs/throttler për logjikën e mirë për pikë fundore dhe për çdo përdorues.

Sa i rëndësishëm është me të vërtetë kompresimi i Brotli në krahasim me gzip? Në ngarkesat tipike të pagesës JSON, 10-20% reduktim shtesë i madhësisë në krahasim me gzip, me CPU pak më të lartë në kompresim (dekompresimi nga ana e klientit është i krahasueshëm).

@defer zëvendëson plotësisht ngarkimin dembel të rrugëve? Jo, ato janë plotësuese: ngarkimi dembel i itinerareve zvogëlon paketën fillestare për seksione të tëra të aplikacionit, @defer zvogëlon koston e paraqitjes së komponentëve individualë të rëndë brenda një faqeje tashmë të ngarkuar.

Si mund të parandaloj që cache Redis të bëhet një pikë e vetme dështimi? Redis në modalitetin Sentinel ose Cluster për disponueshmëri të lartë; në nivelin e aplikacionit, memoria e fshehtë duhet të jetë një optimizim, jo një varësi e vështirë - nëse Redis nuk funksionon, aplikacioni duhet të jetë ende në gjendje të shërbejë (më ngadalë) nga baza e të dhënave.

A është më mirë të shkallëzohet vertikalisht apo horizontalisht? Vertikale (shumë CPU/RAM në një shembull të vetëm) është më e thjeshtë, por ka një tavan dhe një pikë të vetme dështimi. Horizontali (më shumë kopje) shkallëzohet më mirë, por kërkon që aplikacioni të jetë pa shtetësi dhe baza e të dhënave të mos bëhet pengesë.

Sa shpesh duhet të rishikoj indekset e bazës së të dhënave? Sa herë që ndryshojnë modelet mbizotëruese të pyetjeve (karakteristika të reja, filtra të rinj në panelin e kontrollit) dhe periodikisht me pg_stat_user_indexes për të gjetur indekse të pa përdorura që nuk ngadalësohen, shkruan në mënyrë të panevojshme.

A zëvendësojnë sinjalet këndore RxJS? Jo plotësisht: sinjalet janë të shkëlqyera për gjendjen sinkrone lokale të komponentëve; RxJS mbetet më i përshtatshmi për transmetime komplekse asinkrone (debouncing, riprovo, kombinim i transmetimeve të shumta). Modern Angular i bën ato të ndërveprojnë me toSignal()/toObservable().

Cili është gabimi më i kushtueshëm që shoh të përsëritet në projektet NestJS? Pyetjet N+1 nuk u zbuluan derisa grupi i të dhënave të prodhimit të rritet përtej grupit të të dhënave të testit — funksionon mirë me 50 rreshta, koha kalon me 50,000.

A ju nevojitet gjithmonë një CDN edhe për një projekt të vogël? Po për asetet statike (JS, CSS, imazhe): kostoja e konfigurimit është e ulët (shpesh falas) dhe përfitimi i përgjithshëm i vonesës është i menjëhershëm, pavarësisht nga shkalla e projektit.

Përfundim

Performanca nuk është një veçori që shtohet në fund: është shuma e zgjedhjeve të bëra gjatë zhvillimit, nga indeksi në tabelën e duhur te track i harruar në një @for. Pikat që bëjnë vërtet ndryshimin, sipas rendit të ndikimit tipik:

  1. Baza e të dhënave — indekset e sakta dhe bashkimi i lidhjeve me madhësi zgjidhin shumicën e pengesave në fund.
  2. Caching i synuar — Redis në lexime të shpeshta, me anulim të qartë, jo TTL të verbër.
  3. Zbulim efikas i ndryshimeve — sinjale dhe @defer në Angular për të reduktuar punën e panevojshme të paraqitjes.
  4. Mos e blloko ciklin e ngjarjeve — temat e punës për CPU të lidhura, kurrë për I/O.
  5. Mas përpara se të optimizoshSHPJEGON ANALIZË, profilues, standarde reale, jo supozime.

Hapi tjetër natyror për t'u eksploruar është vëzhgimi nga fundi në fund (gjurmimi i shpërndarë me OpenTelemetry) dhe strategjitë e shkallëzimit automatik në Kubernetes, të cilat meritojnë një artikull të veçantë.

Nëse ky udhëzues ishte i dobishëm për ju, ndajeni atë me ekipin tuaj, lini një koment me teknikën që pati ndikimin më të madh në projektin tuaj dhe abonohuni në buletinin në mënyrë që të mos humbisni njohuritë e radhës mbi performancën dhe arkitekturën e plotë.

💬 Shënime nga lexuesit

0 shënime

Shkruaj një shënim

Ndaj mendimin tënd, një sugjerim ose një kompliment

Shënimet e fundit

Ende asnjë shënim. Bëhu i pari që komenton!