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

Performance em Angular, Nestjs e Node.js: O guia definitivo 2026

Uma aplicação full-stack lenta quase nunca é culpa de "uma estrutura lenta". Quase sempre é a soma de dez pequenas decisões erradas: um *ngFor sem trackBy, uma consulta sem índice, um endpoint que não compacta a resposta, um pool de conexões de tamanho aleatório. Tomados individualmente, parecem detalhes. Juntos, eles são a diferença entre um aplicativo que responde em 80 ms e outro que leva 2 segundos sob carga.

Este guia reúne as técnicas que realmente uso na produção em pilha Angular + NestJS + Node.js + PostgreSQL + Redis, com código completo, números de benchmark realistas e os erros mais comuns que vi (e cometi) nos últimos anos.

Índice

  1. Por que o desempenho é importante
  2. Como funciona o fluxo de solicitação-resposta
  3. Pré-requisitos e pilha de referência
  4. Otimizar Angular
  5. Otimizar NestJS
  6. Otimize o Node.js no nível de tempo de execução
  7. Banco de dados: PostgreSQL, índices e pool de conexões
  8. Projeto real: Dashboard com cache Redis
  9. Otimizações avançadas
  10. Referência: antes e depois
  11. Segurança
  12. Práticas recomendadas de arquitetura
  13. 20 erros comuns
  14. FAQ
  15. Conclusão

Por que o desempenho é importante

O que é, na prática. Otimizar o desempenho de uma pilha Angular/NestJS/Node.js significa reduzir três números: o tempo percebido pelo usuário (Core Web Vitals no lado do frontend), o tempo de resposta do backend (p50/p95/p99) e os recursos consumidos para atender cada solicitação (CPU, memória, conexões BD).

Por que isso é importante. O Google usa Core Web Vitals na classificação. A Amazon mediu que cada 100 ms adicionais de latência custa pontos percentuais de conversão. Mas a razão mais concreta, a que vejo todos os dias, é outra: um backend que não escala linearmente força você a comprar hardware em vez de escrever um código melhor - e em certo ponto o hardware não é mais suficiente.

Quando aplicar. Não imediatamente. Otimizar prematuramente um endpoint chamado 10 vezes por dia é uma perda de tempo. As técnicas neste guia devem ser aplicadas quando: você tiver dados reais de criação de perfil (não suposições), o tráfego estiver aumentando ou um endpoint específico aparecer nos logs como um gargalo.

Benefícios. Menores tempos de resposta, custos de infraestrutura reduzidos, melhor SEO, menor rotatividade de usuários, capacidade de lidar com picos de tráfego sem tempo de inatividade.

Desvantagens. Toda otimização tem um custo: mais complexidade (cache para invalidar, trabalhadores para orquestrar), mais superfície de bugs, tempo de desenvolvimento. O cache mal gerenciado é a principal causa de dados obsoletos exibidos aos usuários.

Erros comuns. Otimizar sem medir primeiro; copiar técnicas de blogs sem entender o trade-off; ignorando o banco de dados (a causa número um de lentidão na pilha NestJS) enquanto passa semanas em micro-otimizações Angular.

Casos de uso reais. Um e-commerce que vai de 3s a 400ms na página do produto aumentando as conversões em 15%; um painel interno que expira com 200 usuários simultâneos porque o pool do Postgres está definido para 5 conexões; uma API pública que lida com tráfego 10x após adicionar Redis na frente das consultas mais pesadas.

Como funciona o fluxo de solicitação-resposta

Antes de otimizar, você precisa de um modelo mental de onde o tempo é realmente gasto em uma pilha 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)

Diagrama de sequência para uma solicitação típica (por exemplo, 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

Cada seta neste diagrama é um lugar onde você pode perder – ou ganhar – milissegundos. As seções a seguir os atacam um por um.

Pré-requisitos e pilha de referência

Para seguir os exemplos deste guia você precisa:

Ferramenta Versão recomendada (2026) Notas
Node.js 22 LTS ou superior Suporte nativo para threads de trabalho e busca
Angular 19+ Sinais estáveis, opcional sem zona
NestJS 11+ Suporte Fastify como um adaptador alternativo ao Express
PostgreSQL 16+ Melhores estatísticas do planejador, consultas paralelas
Redis 7+ Suporte para funções granulares e ACLs
Docker / Docker Compose último estável Ambiente local reproduzível
pnpm 9+ Instalar mais rápido que npm, com uso eficiente de disco
Prisma ou TipoORM último estável ORM digitado

Você não precisa de tudo junto desde o primeiro dia. Se você está lendo para otimizar um projeto existente, a seção de banco de dados (índices, pooling) quase sempre deve ser aplicada antes de tudo: é onde está escondido o maior ganho com o menor risco.

Instalação rápida do ambiente de referência

# 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 substitui Express como mecanismo HTTP: com a mesma lógica de aplicativo, Fastify lida com cerca de 20-30% mais solicitações por segundo graças a um roteador mais eficiente e análise JSON - vale a pena adotar em novos projetos.

Otimizar Angular

Sinais e detecção de alterações sem zonas

O problema histórico do Angular é que zone.js intercepta todos os eventos assíncronos (clique, cronômetro, busca) e reinicia a detecção de alterações em toda a árvore de componentes. Em aplicações grandes, isso significa milhares de controles inúteis com um único clique.

Os sinais resolvem o problema na raiz: a estrutura sabe exatamente qual componente depende de quais dados e atualiza apenas isso.

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

Por que funciona: computed() só recalcula quando o sinal do qual depende muda, e Angular apenas atualiza os nós DOM vinculados a esse sinal, sem percorrer todo o componente. Em um aplicativo com zone.js clássico, o mesmo addItem teria acionado a detecção de alterações em toda a subárvore do componente pai.

Bootstrap sem zona (Angular 18+):

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

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

Vantagens: pacote menor (zone.js pesa aproximadamente 30 KB), menos ciclos de detecção de alterações, depuração mais previsível. Cons: Bibliotecas de terceiros que esperam zone.js (algumas versões mais antigas de bibliotecas de materiais ou gráficos) podem exigir NgZone.run() manual. Erro comum: sinais de mistura e referência de objeto mutable — se você alterar uma matriz com .push() em vez de .update(), o sinal não detecta a alteração.

trackBy e @for com chave

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

A nova sintaxe @for requer um track, ao contrário do antigo *ngFor onde trackBy era opcional e muitas vezes esquecido. Sem uma chave estável, o Angular destrói e recria cada nó DOM a cada atualização da lista, em vez de reordenar os existentes - em uma tabela de 500 linhas, é a diferença entre uma atualização instantânea e um clique visível.

Carregamento lento avançado e pré-carregamento estratégico

// 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 é conveniente, mas ingênuo: ele baixa tudo em segundo plano, mesmo que o usuário nunca chegue lá. Uma estratégia baseada em janela de visualização (como quicklink, inspirada no Google) pré-carrega apenas formulários vinculados por elementos realmente visíveis - reduz o desperdício de largura de banda em dispositivos móveis.

SSR e hidratação incremental

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

Benefícios do SSR: Primeira pintura quase instantânea, ótima para SEO e Core Web Vitals (LCP). Cons: custo de CPU do lado do servidor para renderização, necessidade de gerenciar código que roda tanto no Node quanto no navegador (nada window não visto). @defer com na janela de visualização é a técnica mais eficaz em 2026 para componentes pesados (gráficos, editores de rich text, mapas) que não são necessários para a primeira renderização.

Análise de pacote

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

Meta realista para um aplicativo corporativo: pacote inicial com menos de 150 KB compactado. As causas mais comuns de inchaço do pacote: importação de bibliotecas inteiras em vez de funções individuais (import _ from 'lodash' em vez de import debounce from 'lodash/debounce'), moment.js em vez de date-fns, ícones SVG importados como componentes em vez de sprites.

Otimizar NestJS

Compressão HTTP e interceptador de cache

// 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();
A compactação

Brotli (br) reduz as cargas úteis JSON típicas em 70-80% em comparação com o gzip padrão, com um custo de CPU um pouco mais alto na compactação - aceitável para a maioria das APIs REST.

Cache com Redis e gerenciador de cache

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

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

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

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

O que armazenar em cache: respostas públicas, lidas com muito mais frequência do que escritas (catálogos de produtos, listas de artigos de blog, configurações). O que nunca armazenar em cache na camada HTTP: Respostas contendo dados específicos do usuário autenticado sem uma chave de cache que inclua o ID do usuário — é o erro de segurança mais comum com CacheInterceptor.

Invalidação explícita quando os dados mudam:

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

Streaming de respostas grandes

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

Para exportar milhares de linhas, carregar tudo na memória com find() e depois serializar para JSON pode explodir o heap do processo do Node. O streaming grava linha por linha, mantendo o uso de memória constante, independentemente do tamanho do conjunto de dados.

Tubo de validação: preste atenção ao custo

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

class-validator com transform: true use reflect-metadata e reflexão em tempo de execução: em endpoints chamados dezenas de milhares de vezes por segundo (por exemplo, webhooks internos, ingestão de eventos) o custo de validação pode tornar-se mensurável. Nesses casos específicos, uma validação manual leve (ou zod com esquema pré-compilado) é mais rápida.

Otimize o Node.js no nível de tempo de execução

Loop de eventos: nunca bloqueie

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

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

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

Nó. js é de thread único para código JavaScript: uma função síncrona cara (hashing, análise de arquivo enorme, compactação personalizada) bloqueia todas solicitações em andamento, não apenas aquela que a gerou. Threads de trabalho movem o trabalho vinculado à CPU para threads separados, deixando o loop de eventos livre para servir E/S.

Modo Cluster: use todos os núcleos

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

Na produção, prefiro delegar isso ao PM2 (pm2 start dist/main.js -i max) ou orquestrador (réplicas do Kubernetes) em vez de gerenciar clusters manualmente: mesmo benefício, menos código para manter.

Gerenciamento de memória: detectar vazamentos

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

Vazamentos de memória no Node quase sempre surgem de: ouvintes de eventos não removidos, cache na memória sem limite de tamanho (use lru-cache com um max, nunca um Map que cresce indefinidamente), fechamento que contêm referências a objetos grandes, temporizadores (setInterval) nunca são apagados.

Threads de trabalho para trabalho real vinculado à CPU

Tabela de decisão:

Cenário Solução
Consulta de banco de dados, chamada HTTP externa async/await normal (limitado a E/S, não bloqueia)
Hashing de senha, análise de PDF, processamento de imagem Thread de trabalho ou fila de tarefas (BullMQ)
Milhares de pequenos cálculos repetidos Conjunto de threads de trabalho (por exemplo, piscina)
Tarefas longas (minutos), não urgentes Fila de trabalhos assíncronos (Redis + BullMQ), encadeamento não-trabalhador em linha

Banco de dados: PostgreSQL, índices e pool de conexões

Esta é, na minha experiência, a seção que vale mais do que todas as outras juntas. A maioria dos problemas de desempenho que corrigi na produção estavam aqui, não no frontend.

Pool de conexões corrigido

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

Regra geral: connection_limit por instância de backend ≈ (núcleos de CPU do servidor Postgres × 2) / número de réplicas de backend. Com várias réplicas NestJS compartilhando o mesmo Postgres, use PgBouncer na frente do banco de dados no modo de pool transaction para evitar o esgotamento do máximo de conexões do Postgres (padrão 100).

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

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

Índices: a otimização mais impactante

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

Erro comum: Crie um índice de coluna única quando as consultas sempre filtram em duas ou três colunas juntas — um índice composto (user_id, status) também atende consultas apenas em user_id, mas não no oposto.

N+1: O bug de desempenho mais comum com 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 } });

De 100 pedidos, a versão N+1 realiza 101 viagens de ida e volta ao banco de dados. Com latência de rede tão baixa quanto 2 ms por consulta, são 200 ms desperdiçados que um único JOIN elimina completamente.

Paginação: baseado em cursor além do deslocamento

// ❌ 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 força o Postgres a contar e excluir 100.000 linhas antes de retornar o resultado — em tabelas com milhões de registros, a paginação baseada em cursor é muito mais rápida e é o padrão para feeds infinitos e APIs públicas.

Projeto Real: Painel com Redis Cache

Um exemplo mínimo, mas completo, de ponta a ponta: um painel de pedidos mostrando estatísticas agregadas, armazenadas em cache no Redis e invalidadas em eventos de gravação.

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

Lado angular, o painel consome este endpoint com resource() (Angular 19+), que lida com estado de carregamento/erro sem padrão 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()" />
}

Registro e monitoramento mínimos, mas corretos, para este serviço:

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

Na produção, isso deve ser substituído por registro estruturado (pine) e métricas exportadas para Prometheus/Grafana, mas o princípio — medir a duração de cada solicitação — é o mesmo.

Otimizações avançadas

Técnica Quando usar Quando evitar
CDN para ativos estáticos Sempre, para imagens/JS/CSS Nunca deve ser evitado na produção
Limitação de taxa APIs públicas, terminais de autenticação Pontos finais internos de baixa exposição
Cache Redis Leituras frequentes em dados menos voláteis Dados que mudam a cada solicitação
Tópicos de trabalho Tarefas vinculadas à CPU (hashing, imagens) I/O-bound (já tratado por async/await)
Clusters/Múltiplas Réplicas Tráfego saturando um único núcleo Aplicativo de baixo tráfego, sobrecarga desnecessária
Balanceador de carga Várias instâncias de back-end Instância única, sem benefício
Réplica de leitura do banco de dados Leituras >> escritos, reportagens pesadas Conjuntos de dados pequenos, consistência rígida necessária
Escalonamento horizontal Tráfego variável, picos sazonais Gargalos de banco de dados não resolvidos (apenas escala sintomática)

Limitação de taxa com @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) { /* ... */ }

Balanceador de carga (Nginx, upstream para múltiplas instâncias):

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 distribui solicitações para a instância com menos conexões ativas, mais eficaz do que um simples round-robin quando as solicitações têm durações muito diferentes.

Referência: antes e depois

Números coletados com autocannon (100 conexões simultâneas, 30 segundos) em um endpoint GET /api/products?page=1 com aproximadamente 50. 000 linhas na tabela produtos.

Métrico Before (sem cache, sem índice, paginação deslocada) After (Redis + índice + paginação do cursor)
Tempo médio de resposta 420ms 18ms
p95 890ms 35ms
p99 1,450ms 62ms
Solicitações/s 210 4.100
CPU de back-end (média) 78% 22%
Memória RSS 340MB 190 MB
Consulta ao banco de dados para solicitação 1 (lento, ~400ms) ~0,1 (cache atingido 90% das vezes)

Comando usado:

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

Comment: O maior salto vem do índice composto e do cache, não das microotimizações de código. A mudança do Express para o Fastify neste mesmo endpoint gerou mais 15% de solicitações/s, mensuráveis, mas secundários em comparação com o trabalho no banco de dados e no cache.

No frontend, o mesmo princípio: ativar a detecção de alterações sem zona em um painel com 300 componentes reduziu os ciclos de detecção de alterações medidos com Angular DevTools Profiler de ~1.200 para ~80 por interação do usuário, com um tempo para interação que caiu de 3,1s para 1,4s depois de adicionar também @defer nos gráficos abaixo do dobrar.

Segurança

Desempenho e segurança geralmente compartilham as mesmas contramedidas (limitação de taxa, validação de entrada), mas devem ser abordadas explicitamente.

// 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 e token de atualização:

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

Token de acesso de curta duração (15 minutos) limita a janela de risco em caso de roubo; o token de atualização deve ser salvo com hash no lado do servidor e revogável (o logout deve realmente invalidá-lo, não apenas no lado do cliente).

Sanitização e validação com class-validator:

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

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

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

Registro seguro: nunca registre senhas, tokens ou dados confidenciais, mesmo em caso de erro.

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

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

Tratamento centralizado de exceções:

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

Práticas recomendadas de arquitetura

Padrões de repositório e injeção de dependência (nativo em NestJS): Separe a lógica de domínio do acesso aos dados, tornando os serviços testáveis sem um banco de dados real.

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

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

Modularization: cada recurso NestJS como um módulo independente (ProductsModule, OrdersModule) com limites claros; em Angular, apresenta componentes independentes com carregamento lento por limite.

Configuração por ambiente:

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

Validar variáveis de ambiente na inicialização (com Joi ou Zod) evita que um erro de digitação na produção seja descoberto em tempo de execução em vez de na implantação.

Observability: correlaciona logs, métricas e rastreamentos com um request-id propagado de ponta a ponta (cabeçalho X-Request-Id), exportado para uma pilha como Grafana + Loki + Tempo ou equivalente gerenciado.

20 erros comuns

explícito
# Erro Causa Solução
1 Consultas sem índice em colunas frequentemente filtradas Não EXPLAIN ANALYZE durante o desenvolvimento Adicione índices direcionados, verifique com EXPLAIN
2 N+1 consulta com ORM Relacionamentos carregados em loop relations/include para junções explícitas
3 Cache sem invalidação TTL longo "para segurança" Invalidação explícita em eventos de gravação
4 *ngFor/@for sem chave de trilha Copie e cole sem pensar no refluxo do DOM Sempre monitorar item. id
5 Pacote inchado angular Importação de bibliotecas inteiras Importações direcionadas, agitação de árvores, análise de pacotes
6 Bloqueio de loop de eventos Funções síncronas vinculadas à CPU em solicitações HTTP Threads de trabalho ou filas de tarefas
7 Pool de conexão muito pequeno Valor padrão não revisado Tamanho em núcleos de CPU e réplicas
8 Pool de conexão muito grande "Mais é melhor" sem cálculo Respeite o limite máximo do Postgres, use PgBouncer
9 Sem compactação HTTP Esquecido durante a configuração compressão/@fastify/compress em produção
10 PAginação OFFSET em tabelas enormes Padrão mais fácil de implementar Paginação baseada em cursor
11 Validação desabilitada para "velocidade" Percepção incorreta do custo real Meça antes de desativar, use Zod se precisar de velocidade
12 Segredos no registro Registro não filtrado Logger com redação automática de campos sensíveis
13 CORS aberto para * com credenciais Copiado do tutorial Lista branca explícita de origem
14 Sem limite de taxa no login Subestimação do risco de força bruta @nestjs/throttler com limites rígidos em endpoints sensíveis
15 Vazamento de memória dos ouvintes não removido EventoEmissor. ligado sem desligado uma vez() sempre que possível, limpeza explícita
16 Cache de memória ilimitado Mapa crescendo indefinidamente lru-cache com max
17 SSR acessando janela/documento Código não considerado para ambiente de servidor isPlatformBrowser() antes da API somente do navegador
18 Sem índice composto, apenas singles Adicionado um de cada vez sem análise de consulta real Índice composto na ordem real dos filtros
19 Threads de trabalho para tarefas vinculadas a E/S Confusão entre limite de CPU e limite de E/S async/await para E/S, trabalhador somente para CPU
20 Sem tempo limite em chamadas externas Confiança cega em serviços de terceiros Tempos limites explícitos + disjuntor

FAQ

O Fastify é realmente mais rápido que o Express no NestJS? Sim, geralmente 15-30% mais solicitações/seg com a mesma lógica, graças a um roteador e análise mais eficientes. O lucro real depende de quanto tempo seu aplicativo gasta na estrutura HTTP versus em seu código/banco de dados.

Devo sempre usar Redis para armazenamento em cache? Não. Em aplicações pequenas com baixo tráfego, um cache na memória (lru-cache) pode ser suficiente e evita infraestrutura adicional. O Redis se torna necessário quando você tem várias instâncias de back-end que precisam compartilhar o mesmo cache.

O Zoneless Angular está pronto para produção em 2026? Sim para novos projetos ou com dependências atualizadas. Primeiro, verifique se as bibliotecas de terceiros essenciais para o seu projeto não exigem zone.js internamente.

Qual é a diferença entre OFFSET e paginação do cursor? OFFSET pula N linhas em cada página (custo aumentando com N); a paginação do cursor usa o último valor visto como ponto de partida (custo constante, requer classificação estável).

Quantos threads de trabalho devo criar? Não mais que o número de núcleos de CPU disponíveis (os.availableParallelism()); além desse número, você não ganha nenhum paralelismo real, apenas sobrecarga de troca de contexto.

Como escolho o TTL certo para um cache? Dependendo de quanto os dados mudam e de quanto é tolerável mostrar dados um pouco antigos. Dados do catálogo: minutos. Dados financeiros ou de inventário em tempo real: segundos ou invalidação explícita, nunca TTL longo.

Devo usar TypeORM ou Prisma? Prisma oferece digitação mais forte e construtor de consultas mais ergonômico; TypeORM é mais maduro em padrões avançados de Active Record/Data Mapper. Para novos projetos em 2026, tendo a preferir o Prisma pela experiência do desenvolvedor.

O modo cluster do Node também duplica a memória? Sim, cada trabalhador é um processo separado com seu próprio heap. Se seu aplicativo usa 200 MB por instância, 4 trabalhadores significam aproximadamente 800 MB no total - planeje a memória do servidor de acordo.

Quando vale a pena uma réplica de leitura do Postgres? Quando a carga de leitura (painel, relatórios) começa a competir com gravações transacionais na mesma instância. Ele não corrige um banco de dados mal indexado, apenas move o problema.

Como faço para criar o perfil de uma API NestJS? node --prof para criação de perfil de CPU, Chrome DevTools para instantâneos de heap, EXPLAIN ANALYZE para consultas lentas, canhão automático/k6 para testes de carga ponta a ponta.

O Helmet é suficiente para segurança HTTP? É um bom ponto de partida (cabeçalho CSP, HSTS, etc.), mas deve ser integrado com validação de entrada, limitação de taxa, gerenciamento correto de JWT e HTTPS obrigatório.

A limitação de taxa deve ser colocada no nível Nginx ou NestJS? Idealmente, ambos: Nginx para uma primeira defesa bruta contra tráfego anômalo, @nestjs/throttler para lógica precisa por endpoint e por usuário.

Quão importante é realmente a compactação Brotli em comparação com o gzip? Em cargas úteis JSON típicas, redução de tamanho adicional de 10 a 20% em comparação com gzip, com CPU um pouco mais alta na compactação (a descompactação do lado do cliente é comparável).

@defer substitui completamente o carregamento lento de rotas? Não, eles são complementares: o carregamento lento de rotas reduz o pacote inicial para seções inteiras do aplicativo, @defer reduz o custo de renderização de componentes pesados individuais dentro de uma página já carregada.

Como evito que o cache Redis se torne um ponto único de falha? Redis em modo Sentinel ou Cluster para alta disponibilidade; no nível do aplicativo, o cache deve ser uma otimização, não uma dependência rígida — se o Redis estiver inativo, o aplicativo ainda deverá ser capaz de servir (mais lentamente) a partir do banco de dados.

É melhor dimensionar verticalmente ou horizontalmente? Vertical (múltiplas CPU/RAM em uma única instância) é mais simples, mas tem um teto e um único ponto de falha. Horizontal (mais réplicas) é melhor dimensionado, mas exige que o aplicativo seja sem estado e que o banco de dados não se torne um gargalo.

Com que frequência devo revisar os índices do banco de dados? Sempre que os padrões de consulta dominantes mudam (novos recursos, novos filtros no painel) e periodicamente com pg_stat_user_indexes para encontrar índices nunca usados que retardam as gravações desnecessariamente.

Os sinais angulares substituem o RxJS? Não completamente: os sinais são ótimos para o estado síncrono local dos componentes; O RxJS continua sendo mais adequado para fluxos assíncronos complexos (debounce, retry, combinação de vários fluxos). Modern Angular faz com que eles interoperem com toSignal()/toObservable().

Qual é o erro mais caro que vejo recorrente em projetos NestJS? Consultas N+1 não detectadas até que o conjunto de dados de produção ultrapasse o conjunto de dados de teste — funciona bem com 50 linhas, atinge o tempo limite com 50.000.

Você sempre precisa de um CDN, mesmo para um projeto pequeno? Sim, para ativos estáticos (JS, CSS, imagens): o custo de configuração é baixo (geralmente gratuito) e o benefício geral de latência é imediato, independentemente da escala do projeto.

Conclusão

Performance não é um recurso adicionado no final: é a soma das escolhas feitas ao longo do desenvolvimento, desde o índice na tabela à direita até o track esquecido em um @for. Os pontos que realmente fazem a diferença, em ordem de impacto típico:

  1. Database — índices corretos e pool de conexões dimensionados resolvem a maioria dos gargalos de back-end.
  2. Cache direcionado — Redis em leituras frequentes, com invalidação explícita, não TTL cego.
  3. Detecção eficiente de alterações — sinaliza e @defer em Angular para reduzir trabalho de renderização desnecessário.
  4. Não bloquear loop de eventos — threads de trabalho para vinculação à CPU, nunca para E/S.
  5. Medir antes de otimizar — EXPLINAR ANÁLISE, criador de perfil, benchmarks reais, não suposições.

O próximo passo natural a explorar é a observabilidade ponta a ponta (rastreamento distribuído com OpenTelemetry) e estratégias de escalonamento automático no Kubernetes, que merecem um artigo separado.

Se este guia foi útil para você, compartilhe-o com sua equipe, deixe um comentário com a técnica que teve maior impacto no seu projeto e assine a newsletter para não perder os próximos insights sobre performance e arquitetura full-stack.

💬 Notas dos leitores

0 notas

Escreva uma nota

Partilhe a sua opinião, uma sugestão ou um elogio

Notas recentes

Ainda não há notas. Seja o primeiro a comentar!