Analysis and Documentation: - Add ANALISIS-ALINEACION-WORKSPACE-2025-12-08.md with comprehensive gap analysis - Document SIMCO v3.2 system with 20+ directives - Identify alignment gaps between orchestration and projects New SaaS Products Structure: - Create apps/products/pos-micro/ - Ultra basic POS (~100 MXN/month) - Target: Mexican informal market (street vendors, small stores) - Features: Offline-first PWA, WhatsApp bot, minimal DB (~10 tables) - Create apps/products/erp-basico/ - Austere ERP (~300-500 MXN/month) - Target: SMBs needing full ERP without complexity - Features: Inherits from erp-core, modular pricing SaaS Layer: - Create apps/saas/ structure (billing, portal, admin, onboarding) - Add README.md and CONTEXTO-SAAS.md documentation Vertical Alignment: - Verify HERENCIA-ERP-CORE.md exists in all verticals - Add HERENCIA-SPECS-CORE.md to verticals - Update orchestration inventories Updates: - Update WORKSPACE-STATUS.md with new products and analysis - Update suite inventories with new structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
17 KiB
17 KiB
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
-- 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
-- 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
// ❌ 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
// ❌ 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
// ❌ 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
// 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
// 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
// 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
// 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
// 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
// ❌ 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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