Sistema NEXUS v3.4 migrado con: Estructura principal: - core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles) - core/catalog: Catalogo de funcionalidades reutilizables - shared/knowledge-base: Base de conocimiento compartida - devtools/scripts: Herramientas de desarrollo - control-plane/registries: Control de servicios y CI/CD - orchestration/: Configuracion de orquestacion de agentes Proyectos incluidos (11): - gamilit (submodule -> GitHub) - trading-platform (OrbiquanTIA) - erp-suite con 5 verticales: - erp-core, construccion, vidrio-templado - mecanicas-diesel, retail, clinicas - betting-analytics - inmobiliaria-analytics - platform_marketing_content - pos-micro, erp-basico Configuracion: - .gitignore completo para Node.js/Python/Docker - gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git) - Sistema de puertos estandarizado (3005-3199) Generated with NEXUS v3.4 Migration System EPIC-010: Configuracion Git y Repositorios
658 lines
17 KiB
Markdown
658 lines
17 KiB
Markdown
# PATRON DE PERFORMANCE
|
|
|
|
**Version:** 1.0.0
|
|
**Fecha:** 2025-12-08
|
|
**Prioridad:** RECOMENDADA - Seguir para optimizacion
|
|
**Sistema:** SIMCO + CAPVED
|
|
|
|
---
|
|
|
|
## PROPOSITO
|
|
|
|
Definir patrones de optimizacion de rendimiento para todas las capas del sistema, asegurando tiempos de respuesta aceptables y uso eficiente de recursos.
|
|
|
|
---
|
|
|
|
## 1. METRICAS OBJETIVO
|
|
|
|
```
|
|
╔══════════════════════════════════════════════════════════════════════╗
|
|
║ OBJETIVOS DE PERFORMANCE ║
|
|
╠══════════════════════════════════════════════════════════════════════╣
|
|
║ ║
|
|
║ API Response Time: ║
|
|
║ • P50: < 100ms ║
|
|
║ • P95: < 500ms ║
|
|
║ • P99: < 1000ms ║
|
|
║ ║
|
|
║ Database Queries: ║
|
|
║ • Simple query: < 10ms ║
|
|
║ • Complex query: < 100ms ║
|
|
║ • Report query: < 1000ms ║
|
|
║ ║
|
|
║ Frontend: ║
|
|
║ • First Contentful Paint: < 1.5s ║
|
|
║ • Time to Interactive: < 3s ║
|
|
║ • Largest Contentful Paint: < 2.5s ║
|
|
║ ║
|
|
╚══════════════════════════════════════════════════════════════════════╝
|
|
```
|
|
|
|
---
|
|
|
|
## 2. DATABASE PERFORMANCE
|
|
|
|
### 2.1 Indices Efectivos
|
|
|
|
```sql
|
|
-- Indices para columnas frecuentemente filtradas
|
|
CREATE INDEX idx_users_email ON auth.users(email);
|
|
CREATE INDEX idx_users_status ON auth.users(status);
|
|
|
|
-- Indice compuesto para queries frecuentes
|
|
CREATE INDEX idx_orders_user_created
|
|
ON core.orders(user_id, created_at DESC);
|
|
|
|
-- Indice parcial para datos activos
|
|
CREATE INDEX idx_users_active
|
|
ON auth.users(email)
|
|
WHERE status = 'active';
|
|
|
|
-- Indice para busqueda de texto
|
|
CREATE INDEX idx_products_name_gin
|
|
ON core.products USING gin(to_tsvector('spanish', name));
|
|
```
|
|
|
|
### 2.2 Analisis de Queries
|
|
|
|
```sql
|
|
-- Ver plan de ejecucion
|
|
EXPLAIN ANALYZE
|
|
SELECT * FROM orders
|
|
WHERE user_id = 'uuid-123'
|
|
AND created_at > NOW() - INTERVAL '30 days';
|
|
|
|
-- Identificar queries lentas
|
|
SELECT query, calls, mean_time, total_time
|
|
FROM pg_stat_statements
|
|
ORDER BY mean_time DESC
|
|
LIMIT 10;
|
|
```
|
|
|
|
### 2.3 Evitar N+1 Queries
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO: N+1 queries
|
|
async findAllWithOrders(): Promise<User[]> {
|
|
const users = await this.userRepository.find();
|
|
// N queries adicionales para cargar orders
|
|
for (const user of users) {
|
|
user.orders = await this.orderRepository.find({
|
|
where: { userId: user.id }
|
|
});
|
|
}
|
|
return users;
|
|
}
|
|
|
|
// ✅ CORRECTO: Join en una query
|
|
async findAllWithOrders(): Promise<User[]> {
|
|
return this.userRepository.find({
|
|
relations: ['orders'], // TypeORM hace JOIN
|
|
});
|
|
}
|
|
|
|
// ✅ CORRECTO: QueryBuilder con control
|
|
async findAllWithOrders(): Promise<User[]> {
|
|
return this.userRepository
|
|
.createQueryBuilder('user')
|
|
.leftJoinAndSelect('user.orders', 'order')
|
|
.where('user.status = :status', { status: 'active' })
|
|
.orderBy('user.createdAt', 'DESC')
|
|
.getMany();
|
|
}
|
|
```
|
|
|
|
### 2.4 Paginacion Eficiente
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO: OFFSET para paginas grandes
|
|
async findPaginated(page: number, limit: number) {
|
|
return this.repository.find({
|
|
skip: (page - 1) * limit, // Lento en paginas grandes
|
|
take: limit,
|
|
});
|
|
}
|
|
|
|
// ✅ CORRECTO: Cursor-based pagination
|
|
async findPaginatedByCursor(cursor?: string, limit: number = 20) {
|
|
const qb = this.repository
|
|
.createQueryBuilder('item')
|
|
.orderBy('item.createdAt', 'DESC')
|
|
.take(limit + 1); // +1 para saber si hay mas
|
|
|
|
if (cursor) {
|
|
const decodedCursor = this.decodeCursor(cursor);
|
|
qb.where('item.createdAt < :cursor', { cursor: decodedCursor });
|
|
}
|
|
|
|
const items = await qb.getMany();
|
|
const hasMore = items.length > limit;
|
|
|
|
if (hasMore) {
|
|
items.pop(); // Remover el extra
|
|
}
|
|
|
|
return {
|
|
data: items,
|
|
nextCursor: hasMore ? this.encodeCursor(items[items.length - 1]) : null,
|
|
hasMore,
|
|
};
|
|
}
|
|
```
|
|
|
|
### 2.5 Select Solo Campos Necesarios
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO: Traer toda la entidad
|
|
const users = await this.userRepository.find();
|
|
|
|
// ✅ CORRECTO: Solo campos necesarios
|
|
const users = await this.userRepository.find({
|
|
select: ['id', 'email', 'firstName'],
|
|
});
|
|
|
|
// ✅ CORRECTO: Con QueryBuilder
|
|
const users = await this.userRepository
|
|
.createQueryBuilder('user')
|
|
.select(['user.id', 'user.email', 'user.firstName'])
|
|
.getMany();
|
|
```
|
|
|
|
---
|
|
|
|
## 3. BACKEND PERFORMANCE
|
|
|
|
### 3.1 Caching con Redis
|
|
|
|
```typescript
|
|
// src/shared/cache/cache.service.ts
|
|
import { Injectable, Inject } from '@nestjs/common';
|
|
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
|
import { Cache } from 'cache-manager';
|
|
|
|
@Injectable()
|
|
export class CacheService {
|
|
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
|
|
|
|
async get<T>(key: string): Promise<T | null> {
|
|
return this.cacheManager.get<T>(key);
|
|
}
|
|
|
|
async set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {
|
|
await this.cacheManager.set(key, value, ttlSeconds * 1000);
|
|
}
|
|
|
|
async del(key: string): Promise<void> {
|
|
await this.cacheManager.del(key);
|
|
}
|
|
|
|
async delByPattern(pattern: string): Promise<void> {
|
|
const keys = await this.cacheManager.store.keys(pattern);
|
|
await Promise.all(keys.map(key => this.cacheManager.del(key)));
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.2 Cache Decorator
|
|
|
|
```typescript
|
|
// src/shared/decorators/cached.decorator.ts
|
|
import { SetMetadata } from '@nestjs/common';
|
|
|
|
export const CACHE_KEY = 'cache_key';
|
|
export const CACHE_TTL = 'cache_ttl';
|
|
|
|
export const Cached = (key: string, ttlSeconds: number = 300) => {
|
|
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
|
SetMetadata(CACHE_KEY, key)(target, propertyKey, descriptor);
|
|
SetMetadata(CACHE_TTL, ttlSeconds)(target, propertyKey, descriptor);
|
|
};
|
|
};
|
|
|
|
// Interceptor que implementa el caching
|
|
@Injectable()
|
|
export class CacheInterceptor implements NestInterceptor {
|
|
constructor(
|
|
private readonly cacheService: CacheService,
|
|
private readonly reflector: Reflector,
|
|
) {}
|
|
|
|
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
|
|
const cacheKey = this.reflector.get<string>(CACHE_KEY, context.getHandler());
|
|
if (!cacheKey) {
|
|
return next.handle();
|
|
}
|
|
|
|
const cacheTtl = this.reflector.get<number>(CACHE_TTL, context.getHandler()) || 300;
|
|
const request = context.switchToHttp().getRequest();
|
|
const fullKey = `${cacheKey}:${JSON.stringify(request.query)}`;
|
|
|
|
const cached = await this.cacheService.get(fullKey);
|
|
if (cached) {
|
|
return of(cached);
|
|
}
|
|
|
|
return next.handle().pipe(
|
|
tap(async (data) => {
|
|
await this.cacheService.set(fullKey, data, cacheTtl);
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.3 Uso de Cache en Service
|
|
|
|
```typescript
|
|
// src/modules/product/services/product.service.ts
|
|
@Injectable()
|
|
export class ProductService {
|
|
constructor(
|
|
private readonly repository: Repository<ProductEntity>,
|
|
private readonly cacheService: CacheService,
|
|
) {}
|
|
|
|
async findAll(query: ProductQueryDto): Promise<Product[]> {
|
|
const cacheKey = `products:list:${JSON.stringify(query)}`;
|
|
|
|
// Intentar obtener de cache
|
|
const cached = await this.cacheService.get<Product[]>(cacheKey);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
|
|
// Query a BD
|
|
const products = await this.repository.find({
|
|
where: this.buildWhereClause(query),
|
|
take: query.limit,
|
|
});
|
|
|
|
// Guardar en cache (5 minutos)
|
|
await this.cacheService.set(cacheKey, products, 300);
|
|
|
|
return products;
|
|
}
|
|
|
|
async update(id: string, dto: UpdateProductDto): Promise<Product> {
|
|
const product = await this.repository.save({ id, ...dto });
|
|
|
|
// Invalidar cache relacionado
|
|
await this.cacheService.delByPattern('products:*');
|
|
|
|
return product;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.4 Compresion de Responses
|
|
|
|
```typescript
|
|
// src/main.ts
|
|
import * as compression from 'compression';
|
|
|
|
async function bootstrap() {
|
|
const app = await NestFactory.create(AppModule);
|
|
|
|
// Comprimir responses > 1kb
|
|
app.use(compression({
|
|
threshold: 1024,
|
|
level: 6, // Balance entre compresion y CPU
|
|
}));
|
|
|
|
await app.listen(3000);
|
|
}
|
|
```
|
|
|
|
### 3.5 Lazy Loading de Modulos
|
|
|
|
```typescript
|
|
// Cargar modulo pesado solo cuando se necesita
|
|
@Module({
|
|
imports: [
|
|
// Modulo de reportes cargado lazy
|
|
RouterModule.register([
|
|
{
|
|
path: 'reports',
|
|
module: ReportsModule,
|
|
},
|
|
]),
|
|
],
|
|
})
|
|
export class AppModule {}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. FRONTEND PERFORMANCE
|
|
|
|
### 4.1 Code Splitting
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO: Importar todo
|
|
import { HeavyComponent } from './HeavyComponent';
|
|
|
|
// ✅ CORRECTO: Lazy loading
|
|
const HeavyComponent = lazy(() => import('./HeavyComponent'));
|
|
|
|
// Uso con Suspense
|
|
<Suspense fallback={<LoadingSpinner />}>
|
|
<HeavyComponent />
|
|
</Suspense>
|
|
```
|
|
|
|
### 4.2 Memoizacion
|
|
|
|
```typescript
|
|
// React.memo para componentes puros
|
|
const UserCard = memo(({ user }: { user: User }) => {
|
|
return (
|
|
<div>
|
|
<h3>{user.name}</h3>
|
|
<p>{user.email}</p>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
// useMemo para calculos costosos
|
|
const ExpensiveList = ({ items, filter }: Props) => {
|
|
const filteredItems = useMemo(
|
|
() => items.filter(item => complexFilter(item, filter)),
|
|
[items, filter], // Solo recalcular si cambian
|
|
);
|
|
|
|
return <ul>{filteredItems.map(/* ... */)}</ul>;
|
|
};
|
|
|
|
// useCallback para funciones estables
|
|
const ParentComponent = () => {
|
|
const [count, setCount] = useState(0);
|
|
|
|
const handleClick = useCallback(() => {
|
|
console.log('clicked');
|
|
}, []); // Funcion estable
|
|
|
|
return <ChildComponent onClick={handleClick} />;
|
|
};
|
|
```
|
|
|
|
### 4.3 Virtualizacion de Listas
|
|
|
|
```typescript
|
|
// Para listas largas, usar virtualizacion
|
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
|
|
const VirtualList = ({ items }: { items: Item[] }) => {
|
|
const parentRef = useRef<HTMLDivElement>(null);
|
|
|
|
const virtualizer = useVirtualizer({
|
|
count: items.length,
|
|
getScrollElement: () => parentRef.current,
|
|
estimateSize: () => 50, // Altura estimada de cada item
|
|
});
|
|
|
|
return (
|
|
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
|
|
<div style={{ height: virtualizer.getTotalSize() }}>
|
|
{virtualizer.getVirtualItems().map(virtualItem => (
|
|
<div
|
|
key={virtualItem.key}
|
|
style={{
|
|
position: 'absolute',
|
|
top: virtualItem.start,
|
|
height: virtualItem.size,
|
|
}}
|
|
>
|
|
<ItemComponent item={items[virtualItem.index]} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
### 4.4 Optimizacion de Imagenes
|
|
|
|
```typescript
|
|
// Componente de imagen optimizada
|
|
const OptimizedImage = ({
|
|
src,
|
|
alt,
|
|
width,
|
|
height,
|
|
}: ImageProps) => {
|
|
return (
|
|
<img
|
|
src={src}
|
|
alt={alt}
|
|
width={width}
|
|
height={height}
|
|
loading="lazy" // Lazy loading nativo
|
|
decoding="async" // Decodificacion asincrona
|
|
style={{ aspectRatio: `${width}/${height}` }} // Prevenir layout shift
|
|
/>
|
|
);
|
|
};
|
|
|
|
// Con srcset para responsive
|
|
const ResponsiveImage = ({ src, alt }: Props) => {
|
|
return (
|
|
<img
|
|
src={src}
|
|
alt={alt}
|
|
srcSet={`
|
|
${src}?w=400 400w,
|
|
${src}?w=800 800w,
|
|
${src}?w=1200 1200w
|
|
`}
|
|
sizes="(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px"
|
|
loading="lazy"
|
|
/>
|
|
);
|
|
};
|
|
```
|
|
|
|
### 4.5 Debounce y Throttle
|
|
|
|
```typescript
|
|
// src/shared/hooks/useDebounce.ts
|
|
import { useState, useEffect } from 'react';
|
|
|
|
export function useDebounce<T>(value: T, delay: number): T {
|
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
|
return () => clearTimeout(timer);
|
|
}, [value, delay]);
|
|
|
|
return debouncedValue;
|
|
}
|
|
|
|
// Uso en busqueda
|
|
const SearchInput = () => {
|
|
const [search, setSearch] = useState('');
|
|
const debouncedSearch = useDebounce(search, 300);
|
|
|
|
// Query solo se ejecuta cuando debouncedSearch cambia
|
|
const { data } = useQuery({
|
|
queryKey: ['search', debouncedSearch],
|
|
queryFn: () => api.search(debouncedSearch),
|
|
enabled: debouncedSearch.length > 2,
|
|
});
|
|
|
|
return <input value={search} onChange={e => setSearch(e.target.value)} />;
|
|
};
|
|
```
|
|
|
|
### 4.6 React Query - Cache y Stale Time
|
|
|
|
```typescript
|
|
// src/shared/hooks/useProducts.ts
|
|
export const useProducts = (filters: ProductFilters) => {
|
|
return useQuery({
|
|
queryKey: ['products', filters],
|
|
queryFn: () => productService.getAll(filters),
|
|
staleTime: 5 * 60 * 1000, // 5 minutos antes de refetch
|
|
gcTime: 30 * 60 * 1000, // 30 minutos en cache
|
|
placeholderData: keepPreviousData, // Mostrar datos anteriores mientras carga
|
|
});
|
|
};
|
|
|
|
// Prefetch para navegacion anticipada
|
|
const ProductList = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
const handleMouseEnter = (productId: string) => {
|
|
// Prefetch detalle del producto
|
|
queryClient.prefetchQuery({
|
|
queryKey: ['product', productId],
|
|
queryFn: () => productService.getById(productId),
|
|
});
|
|
};
|
|
|
|
return (/* ... */);
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 5. API DESIGN PARA PERFORMANCE
|
|
|
|
### 5.1 Campos Seleccionables
|
|
|
|
```typescript
|
|
// Permitir al cliente elegir campos
|
|
@Get()
|
|
async findAll(
|
|
@Query('fields') fields?: string, // ?fields=id,name,price
|
|
): Promise<Partial<Product>[]> {
|
|
const select = fields?.split(',') || undefined;
|
|
return this.productService.findAll({ select });
|
|
}
|
|
```
|
|
|
|
### 5.2 Expansion de Relaciones
|
|
|
|
```typescript
|
|
// Permitir expansion opcional de relaciones
|
|
@Get(':id')
|
|
async findOne(
|
|
@Param('id') id: string,
|
|
@Query('expand') expand?: string, // ?expand=category,reviews
|
|
): Promise<Product> {
|
|
const relations = expand?.split(',') || [];
|
|
return this.productService.findOne(id, { relations });
|
|
}
|
|
```
|
|
|
|
### 5.3 Batch Endpoints
|
|
|
|
```typescript
|
|
// Endpoint para multiples operaciones
|
|
@Post('batch')
|
|
async batchCreate(@Body() dtos: CreateProductDto[]): Promise<Product[]> {
|
|
// Una transaccion en lugar de N requests
|
|
return this.productService.createMany(dtos);
|
|
}
|
|
|
|
// Endpoint para multiples IDs
|
|
@Get('batch')
|
|
async batchGet(@Query('ids') ids: string): Promise<Product[]> {
|
|
const idArray = ids.split(',');
|
|
return this.productService.findByIds(idArray);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. MONITORING Y PROFILING
|
|
|
|
### 6.1 Metricas de API
|
|
|
|
```typescript
|
|
// src/shared/interceptors/metrics.interceptor.ts
|
|
@Injectable()
|
|
export class MetricsInterceptor implements NestInterceptor {
|
|
constructor(private readonly metricsService: MetricsService) {}
|
|
|
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
const request = context.switchToHttp().getRequest();
|
|
const { method, url } = request;
|
|
const startTime = Date.now();
|
|
|
|
return next.handle().pipe(
|
|
tap({
|
|
next: () => {
|
|
const duration = Date.now() - startTime;
|
|
this.metricsService.recordRequest(method, url, 200, duration);
|
|
},
|
|
error: (error) => {
|
|
const duration = Date.now() - startTime;
|
|
this.metricsService.recordRequest(method, url, error.status || 500, duration);
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6.2 Query Logging Condicional
|
|
|
|
```typescript
|
|
// Solo loguear queries lentas en produccion
|
|
const typeOrmConfig: TypeOrmModuleOptions = {
|
|
logging: process.env.NODE_ENV === 'production' ? ['error', 'warn'] : true,
|
|
maxQueryExecutionTime: 1000, // Loguear queries > 1s
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 7. CHECKLIST DE PERFORMANCE
|
|
|
|
```
|
|
Database:
|
|
[ ] Indices en columnas de WHERE frecuentes
|
|
[ ] Indices compuestos para queries comunes
|
|
[ ] No N+1 queries (usar JOIN/relations)
|
|
[ ] Paginacion cursor-based para datasets grandes
|
|
[ ] SELECT solo campos necesarios
|
|
[ ] EXPLAIN ANALYZE en queries criticas
|
|
|
|
Backend:
|
|
[ ] Cache implementado para datos frecuentes
|
|
[ ] Invalidacion de cache correcta
|
|
[ ] Compresion habilitada
|
|
[ ] Connection pooling configurado
|
|
[ ] Timeouts apropiados
|
|
|
|
Frontend:
|
|
[ ] Code splitting / lazy loading
|
|
[ ] Memoizacion donde corresponde
|
|
[ ] Virtualizacion para listas largas
|
|
[ ] Imagenes optimizadas y lazy loaded
|
|
[ ] Debounce en inputs de busqueda
|
|
[ ] React Query con staleTime apropiado
|
|
|
|
API:
|
|
[ ] Paginacion en endpoints de listas
|
|
[ ] Campos seleccionables (opcional)
|
|
[ ] Batch endpoints para operaciones multiples
|
|
[ ] Rate limiting para proteger recursos
|
|
```
|
|
|
|
---
|
|
|
|
**Version:** 1.0.0 | **Sistema:** SIMCO | **Tipo:** Patron de Performance
|