# Regla: Performance

La optimización prematura es la raíz de todos los males — Donald Knuth.
Esta regla equilibra ese principio con las restricciones reales del sistema:
algunos problemas de performance son predecibles y deben evitarse desde el diseño.

---

## No optimizar prematuramente

- No escribir código complicado "por si acaso es lento".
  Escribir el código más claro y simple primero.
- No sacrificar legibilidad por micro-optimizaciones sin evidencia de que
  el código es un cuello de botella real.
- El código simple es más fácil de optimizar después que el código complejo
  de hacer legible después.
- Excepción: anti-patrones de performance con costo predecible y conocido
  (N+1 queries, carga de archivos completos en memoria, loops anidados O(n²))
  deben evitarse desde el diseño aunque no haya medición todavía.

---

## Medir antes de optimizar

- Antes de cualquier optimización: tener una medición de baseline.
  Sin número, no hay optimización — hay cambio ciego.
- Formato mínimo de documentación de optimización:
  ```
  Problema: el endpoint /facturas tarda 4.2s promedio bajo carga de 100 req/s
  Causa identificada: N+1 queries en la relación factura → items (profiled con py-spy)
  Solución: eager loading con selectinload()
  Resultado: 0.18s promedio (mejora 23x)
  ```
- Optimización sin medición: no se acepta en code review.
- Las métricas de performance son parte del PR si el PR es una optimización.

---

## Profiling con herramientas

Python — herramientas aprobadas:
- `py-spy` para profiling en producción (sin reiniciar el proceso)
- `cProfile` + `snakeviz` para profiling en desarrollo
- `line_profiler` para análisis línea por línea
- `memory_profiler` para problemas de memoria
- `EXPLAIN ANALYZE` en PostgreSQL para queries lentas
- `sqlalchemy echo=True` para ver queries generadas en desarrollo

TypeScript/Angular — herramientas aprobadas:
- Chrome DevTools Performance tab
- Angular DevTools (profiler de componentes y change detection)
- `webpack-bundle-analyzer` para tamaño de bundles
- Lighthouse para métricas web (LCP, FID, CLS)

Reglas de profiling:
- Perfilar en condiciones similares a producción (no en localhost con 1 usuario).
- Identificar el 20% del código que consume el 80% del tiempo antes de tocar nada.
- Documentar el resultado del profiling en el ticket o PR.

---

## N+1 queries — prohibidos

El problema N+1 es un bug de performance predecible y evitable desde el diseño:

Síntoma:
```python
# MALO: genera N+1 queries
facturas = db.query(Factura).all()
for factura in facturas:
    print(factura.cliente.nombre)  # query adicional por cada factura
```

Solución con eager loading:
```python
# BIEN: 2 queries totales independientemente de cuántas facturas
facturas = db.query(Factura).options(selectinload(Factura.cliente)).all()
for factura in facturas:
    print(factura.cliente.nombre)  # ya cargado
```

Reglas estrictas:
- Todo endpoint que devuelve colecciones con relaciones DEBE usar eager loading.
- Revisión obligatoria en code review: buscar acceso a atributos de relaciones
  dentro de loops.
- En testing: activar logging de SQL en tests de integración para detectar N+1
  contando queries ejecutadas.
- `lazy="selectin"` en relaciones ORM frecuentemente accedidas como default seguro.

---

## Lazy loading para módulos pesados

Frontend Angular:
- Todo módulo que no es parte del critical rendering path DEBE cargarse con lazy loading.
- Rutas de admin, reportes, configuración: siempre lazy.
- Formato obligatorio en el router:
  ```typescript
  {
    path: 'reportes',
    loadChildren: () => import('./reportes/reportes.module')
      .then(m => m.ReportesModule)
  }
  ```
- Verificar el tamaño del bundle inicial con `webpack-bundle-analyzer`.
  Bundle inicial objetivo: <500KB gzipped.
- Preloading strategy para módulos que el usuario probablemente visitará:
  `PreloadAllModules` o custom `QuicklinkStrategy`.

Backend Python:
- Imports costosos (modelos de ML, librerías de procesamiento) dentro de las
  funciones que los usan, no en el nivel de módulo.
- Conexiones a servicios externos: inicializar en el primer uso, no al arrancar.

---

## Caching con invalidación explícita

El caching sin invalidación es un bug en espera:

Reglas obligatorias:
- Todo cache tiene un TTL explícito definido en código o configuración.
  NUNCA TTL infinito en producción.
- Los casos de invalidación anticipada están documentados en el código.
- El cache key incluye todos los parámetros que afectan el resultado.
  Un cache key incorrecto causa bugs silenciosos de datos obsoletos.

Niveles de caching y herramientas:
- **In-memory (proceso)**: `functools.lru_cache` / `@cache` para valores
  computacionalmente costosos e inmutables. Documentar el tamaño máximo.
- **Distribuido**: Redis para datos compartidos entre instancias.
  Usar prefijos de key por entorno (`prod:`, `staging:`).
- **HTTP**: Cache-Control headers explícitos. `no-store` para datos sensibles.
  `max-age` con revalidación para recursos estáticos.
- **BD query cache**: Con precaución — Postgres no tiene query cache nativo.
  Usar vistas materializadas para queries complejas y costosas.

Patrón de invalidación:
```python
CACHE_KEY_FACTURAS = "facturas:{empresa_id}:{mes}"
CACHE_TTL_FACTURAS = 300  # 5 minutos

# Al mutar datos: invalidar explícitamente
async def crear_factura(empresa_id: UUID, ...):
    factura = await _persistir_factura(...)
    await cache.delete(CACHE_KEY_FACTURAS.format(empresa_id=empresa_id, mes=...))
    return factura
```

---

## Índices de BD documentados

- Todo índice creado en la BD tiene un comentario en la migración que explica
  qué query lo justifica.
- Formato obligatorio en la migración:
  ```python
  # Índice para la query: GET /facturas?empresa_id=X&estatus=Y
  # Antes del índice: 450ms (seq scan 2M filas)
  # Después: 3ms (index scan)
  op.create_index('ix_facturas_empresa_estatus', 'facturas',
                  ['empresa_id', 'estatus'])
  ```
- No crear índices "preventivos" sin query que los justifique.
- Revisar índices no utilizados en producción cada 3 meses:
  ```sql
  SELECT * FROM pg_stat_user_indexes WHERE idx_scan = 0;
  ```
- Índices compuestos: el orden de columnas importa. La columna más selectiva
  o la usada en igualdad va primero.

---

## Métricas de performance objetivo

Definir por proyecto, documentar en el README. Valores de referencia razonables:

| Tipo de operación | Objetivo | Alerta |
|-------------------|----------|--------|
| API GET simple | <100ms p95 | >500ms |
| API POST con escritura en BD | <300ms p95 | >1s |
| API con agregaciones | <500ms p95 | >2s |
| Render inicial de página | <2s LCP | >4s |
| Bundle JavaScript inicial | <500KB gzip | >1MB |

---

## Checklist de performance antes de merge

- [ ] Sin acceso a relaciones ORM dentro de loops (N+1)
- [ ] Módulos pesados en Angular con lazy loading
- [ ] Todo cache nuevo tiene TTL explícito e invalidación documentada
- [ ] Índices nuevos documentados en la migración con query que los justifica
- [ ] Si el PR es una optimización: tiene medición antes/después
- [ ] Sin imports costosos a nivel de módulo que retrasen el arranque
