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
- Pse ka rëndësi performanca
- Si funksionon rrjedha e kërkesë-përgjigje
- Parakushtet dhe rafti i referencës
- Optimizo Angular
- Optimizo NestJS
- Optimizo Node.js në nivelin e ekzekutimit
- Baza e të dhënave: PostgreSQL, indekset dhe bashkimi i lidhjeve
- Projekt real: Pult me cache Redis
- Optimizime të avancuara
- Benchmark: para dhe pas
- Siguria
- Praktikat më të mira arkitekturore
- 20 gabime të zakonshme
- FAQ
- 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:
- Baza e të dhënave — indekset e sakta dhe bashkimi i lidhjeve me madhësi zgjidhin shumicën e pengesave në fund.
- Caching i synuar — Redis në lexime të shpeshta, me anulim të qartë, jo TTL të verbër.
- Zbulim efikas i ndryshimeve — sinjale dhe
@defer në Angular për të reduktuar punën e panevojshme të paraqitjes.
- Mos e blloko ciklin e ngjarjeve — temat e punës për CPU të lidhura, kurrë për I/O.
- Mas përpara se të optimizosh —
SHPJEGON 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ë.