C1: Materialized view part_vehicle_preview (creación en progreso) - Migración v3.3_materialized_view.sql - catalog_service.py y dashboard/server.py ahora usan la MV - Script refresh_part_vehicle_preview.py + warm_vehicle_cache.py actualizado C2: Fix cache warming script (autónomo) - Auto-re-ejecuta con sudo -u postgres si peer auth falla - Args CLI: --dsn, --batch-size, --ttl, --dry-run C3: CSS dinámico residual extraído - sidebar.js → sidebar.css (nuevo) - pos-utils.js → common.css (nuevo) - Links agregados a 14 templates POS C4: Script de load testing básico - scripts/load_test.py: métricas p50/p95/p99, throughput, errores C5: Documentación actualizada - FASES_IMPLEMENTADAS.md: test count real, FASE 7 completa - performance_audit_2026.md: anexo post-FASE 7, métricas actualizadas A1: Serialización orjson - pos/json_provider.py: DefaultJSONProvider con orjson.dumps/loads - Aplicado a POS app y Dashboard server - Fix indentation error en pos_bp.py Tests: 73/73 pasando
23 KiB
Nexus Autoparts — Auditoría de Performance y Optimización
Fecha: 2026-04-26
Versión del sistema: FASE 6 completa + Opción C + FASE 7 performance
Auditor realizado por: Análisis automatizado de codebase + métricas del sistema
1. Resumen Ejecutivo
El sistema presenta cuellos de botella severos concentrados en tres áreas:
| Área | Severidad | Impacto estimado |
|---|---|---|
| Base de datos | 🔴 Crítica | Respuestas de catálogo de 500ms–5s; riesgo de caída bajo carga concurrente |
| Frontend (POS) | 🟠 Alta | First paint lento, memory leaks, transferencia innecesaria de ~400 KB/página |
| Infraestructura | 🟠 Alta | Sin compresión, sin pooling de conexiones, Redis subutilizado |
| Arquitectura Python | 🟡 Media | Sync-only, cache por proceso, respuestas API no optimizadas |
Métricas clave del sistema actual
| Métrica | Valor | Umbral saludable |
|---|---|---|
Tamaño tabla vehicle_parts |
254 GB (~2.05B filas) | < 50 GB o particionada |
Índices vehicle_parts |
149 GB (más que la tabla) | < 50% del tamaño de tabla |
| Tamaño total DB maestra | 257 GB | — |
| Redis utilizado | 840 KB | > 50 MB (cache activa) |
| JS transferido por página POS | ~526 KB (sin comprimir) | < 150 KB con gzip |
| CSS inline duplicado por página | ~247 KB | < 20 KB (compartido) |
| Connection pooling | Ninguno | Sí (min 2, max 10–20) |
| Compresión HTTP (gzip/brotli) | Deshabilitada | Sí |
Nota (2026-04-27): FASE 7 implementada. Los valores anteriores reflejan el estado pre-optimización. Ver Anexo FASE 7 para estado actual.
2. Base de Datos — Análisis Detallado
2.1 El elefante en la habitación: vehicle_parts (254 GB)
Esta tabla contiene la relación entre vehículos (model_year_engine) y piezas (parts). Es el corazón del catálogo y su tamaño es extremo.
Tabla: 105 GB
Índices: 149 GB
Total: 254 GB
Filas: ~2,053,774,208 (estimado)
Índices actuales:
vehicle_parts_pkey — btree (id_vehicle_part)
idx_vehicle_parts_part — btree (part_id)
idx_vehicle_parts_mye — btree (model_year_engine_id)
uq_vehicle_part_mye_part — partial unique (model_year_engine_id, part_id)
Problemas:
- Sin particionamiento por
part_idomodel_year_engine_id - Índices ocupan más espacio que la tabla (indicativo de índices anchos o muchos índices secundarios)
- El índice
idx_vehicle_parts_partprobablemente se usa para queries que filtran porpart_id = ANY(...), pero sin un índice compuesto(part_id, model_year_engine_id)forza lookups adicionales COUNT(*)en esta tabla, aunque indexado, requiere verificar visibilidad de tuplas y puede ser lento a esta escala
Recomendación inmediata: Particionar vehicle_parts por rango de part_id (ej. cada 10M IDs) o por hash. Esto reduce el tamaño de índice por partición y permite paralelismo en consultas. Requiere planificación cuidadosa por el impacto en inserts.
2.2 Connection Pooling — Inexistente (🔴 Crítico)
Archivo: pos/tenant_db.py
def get_master_conn():
return psycopg2.connect(MASTER_DB_URL) # Conexión TCP nueva CADA request
def get_tenant_conn(tenant_id):
...
return psycopg2.connect(TENANT_DB_URL_TEMPLATE.format(db_name=db_name))
Impacto:
- Cada request HTTP abre mínimo 1 conexión TCP (a veces 2: master + tenant)
- Overhead de handshake TCP + SSL + auth de PostgreSQL (~5–20ms por conexión)
- Bajo carga concurrente (> 10 usuarios), PostgreSQL puede agotar
max_connections(default 100) - Gunicorn corre con
workers = cpu_count * 2 + 1(~15 workers). Cada worker puede tener múltiples requests concurrentes = explosión de conexiones
Fix: Implementar psycopg2.pool.ThreadedConnectionPool:
from psycopg2 import pool
_master_pool = None
_tenant_pools = {}
def get_master_conn():
global _master_pool
if _master_pool is None:
_master_pool = pool.ThreadedConnectionPool(
minconn=2, maxconn=10, dsn=MASTER_DB_URL
)
return _master_pool.getconn()
def release_master_conn(conn):
if _master_pool:
_master_pool.putconn(conn)
Impacto esperado: 30–50% reducción en latencia de API, eliminación de errores de "too many connections".
2.3 Full Table Scans en warehouse_inventory (🔴 Crítico)
Archivo: pos/services/catalog_service.py:964-969
stock_per_oem AS (
SELECT part_id, COUNT(*) AS bodega_count, MIN(price) AS min_price,
SUM(stock_quantity) AS total_stock
FROM warehouse_inventory
WHERE stock_quantity > 0
GROUP BY part_id
)
Esta CTE corre en cada página del catálogo Local. Aunque warehouse_inventory no sea gigante hoy, escanea toda la tabla sin índice adecuado.
Fix inmediato:
-- Índice parcial para stock positivo
CREATE INDEX idx_wi_part_stock_positive
ON warehouse_inventory(part_id)
WHERE stock_quantity > 0;
Además, reemplazar la CTE por una subquery que filtre por part_id = ANY(...):
SELECT part_id, COUNT(*) AS bodega_count, MIN(price) AS min_price,
SUM(stock_quantity) AS total_stock
FROM warehouse_inventory
WHERE part_id = ANY(%s) AND stock_quantity > 0
GROUP BY part_id
2.4 Agregaciones sin límites en inventory_operations (🟠 Alta)
Múltiples archivos: inventory_engine.py, inventory_vehicle_compat.py, peer_service.py
Patrón recurrente:
SELECT inventory_id, SUM(quantity) as stock
FROM inventory_operations
GROUP BY inventory_id
inventory_operations es append-only. Cada venta, compra, ajuste, transferencia agrega filas. Esta agregación escanea todas las operaciones históricas.
Fix recomendado:
-
Corto plazo: Agregar índice compuesto y filtrar por fecha reciente si el negocio no requiere stock histórico:
CREATE INDEX idx_inv_ops_inventory_branch_created ON inventory_operations(inventory_id, branch_id, created_at DESC); -
Mediano plazo: Mantener una tabla de
inventory_stock_summaryactualizada por triggers:CREATE TABLE inventory_stock_summary ( inventory_id INT PRIMARY KEY, branch_id INT, stock INT NOT NULL DEFAULT 0, last_updated TIMESTAMPTZ DEFAULT NOW() );Trigger en
inventory_operationsque actualiza el sumario en tiempo real. Las lecturas pasan deO(n operaciones)aO(1).
2.5 smart_search() — DISTINCT ON sobre 2B filas (🔴 Crítico)
Archivo: pos/services/catalog_service.py:1339-1349
SELECT DISTINCT ON (vp.part_id)
vp.part_id, b.name_brand, m.name_model, y.year_car
FROM vehicle_parts vp
JOIN model_year_engine mye ON mye.id_mye = vp.model_year_engine_id
JOIN models m ON m.id_model = mye.model_id
JOIN brands b ON b.id_brand = m.brand_id
JOIN years y ON y.id_year = mye.year_id
WHERE vp.part_id = ANY(%s)
ORDER BY vp.part_id, y.year_car DESC
Para búsquedas que retornan 20–50 parts, esta query hace JOIN con una tabla de 2B filas para obtener "el vehículo más reciente" de cada pieza.
Fix: Precalcular esta relación en una tabla materializada o cachear en Redis:
CREATE MATERIALIZED VIEW part_vehicle_preview AS
SELECT DISTINCT ON (vp.part_id)
vp.part_id, b.name_brand, m.name_model, y.year_car
FROM vehicle_parts vp
JOIN model_year_engine mye ON mye.id_mye = vp.model_year_engine_id
JOIN models m ON m.id_model = mye.model_id
JOIN brands b ON b.id_brand = m.brand_id
JOIN years y ON y.id_year = mye.year_id
ORDER BY vp.part_id, y.year_car DESC;
CREATE UNIQUE INDEX idx_pvp_part ON part_vehicle_preview(part_id);
REFRESH MATERIALIZED VIEW CONCURRENTLY part_vehicle_preview;
2.6 N+1 Queries en ventas (🟠 Alta)
Archivo: pos/services/pos_engine.py:331-333
for item in totals['items']:
cur.execute("SELECT retail_price FROM inventory WHERE id = %s", (item['inventory_id'],))
rp_row = cur.fetchone()
Una venta de 20 ítems = 20 queries adicionales.
Fix: Cargar todos los precios en una sola query antes del loop:
inv_ids = [item['inventory_id'] for item in totals['items']]
cur.execute("SELECT id, retail_price FROM inventory WHERE id = ANY(%s)", (inv_ids,))
price_map = {row[0]: row[1] for row in cur.fetchall()}
2.7 Índices faltantes críticos
| Tabla | Índice faltante | Queries afectadas |
|---|---|---|
warehouse_inventory |
(part_id) WHERE stock_quantity > 0 |
get_parts_local, get_part_detail |
warehouse_inventory |
(part_id, stock_quantity) |
Todas las bodega queries |
inventory_operations |
(inventory_id, branch_id, created_at DESC) |
Stock + historial |
inventory |
(branch_id, is_active, part_number) |
Stock local lookups |
parts |
(name_part text_pattern_ops) |
get_part_types, _shop_supplies |
3. Frontend — Análisis Detallado
3.1 JavaScript — Problemas críticos
innerHTML += en loops (🔴 Crítico)
Archivo: catalog.js líneas ~1578, 1584, 1608, etc.
years.forEach(function (y) {
vsYear.innerHTML += '<option value="' + y.id_year + '">' + y.year_car + '</option>';
});
Cada iteración fuerza al browser a reparsear y re-renderizar todo el subtree. Complejidad O(N²).
Fix:
vsYear.innerHTML = '<option value="">Año...</option>' +
years.map(y => `<option value="${y.id_year}">${y.year_car}</option>`).join('');
Memory leaks por event listeners (🔴 Crítico)
Archivo: catalog.js líneas 261–275 (breadcrumb), 1467–1475 (cart)
Cada vez que se re-renderiza el breadcrumb o el carrito, se reemplaza innerHTML y se adjuntan nuevos listeners. Los listeners antiguos quedan huérfanos en memoria.
Fix: Event delegation — adjuntar UNA vez en el contenedor padre:
breadcrumb.addEventListener('click', function (e) {
var el = e.target.closest('[data-bc-action]');
if (!el) return;
// dispatch
});
Race conditions por falta de AbortController (🟠 Alta)
Navegación rápida por catálogo + red lenta = respuestas desordenadas sobrescriben la UI.
Fix:
var currentAbort = null;
function apiFetch(url) {
if (currentAbort) currentAbort.abort();
currentAbort = new AbortController();
return fetch(url, { signal: currentAbort.signal }).then(r => r.json());
}
3.2 CSS Inline masivo (🔴 Crítico)
Cada template HTML duplica CSS:
| Página | Tamaño HTML | CSS inline estimado |
|---|---|---|
catalog.html |
49 KB | ~570 líneas |
inventory.html |
73 KB | ~2,000 líneas |
pos.html |
73 KB | ~1,000 líneas |
Impacto:
- Cambiar un color requiere editar 10+ archivos
- CSS no se cachea entre páginas (va dentro del HTML)
- First paint retrasado parseando CSS redundante
Fix: Extraer todo CSS a pos/static/css/common.css y archivos temáticos. Reducción estimada: 60–80% del payload HTML.
3.3 Assets sin optimizar (🟠 Alta)
- Sin minificación: 526 KB de JS sin comprimir
- Sin gzip/brotli en nginx: Reducción de 40–60% gratis
- Sin
defer/async: 10 scripts bloquean el parser HTML - Sin lazy loading de imágenes: Todas las imágenes de piezas se cargan inmediatamente
- Cache-busting destructivo:
?t=Date.now()en imágenes de inventario anula cache del browser
Fix rápido (1 hora):
# En nginx/nexus-pos.conf
gzip on;
gzip_types text/css application/javascript application/json;
gzip_min_length 1024;
<!-- En templates HTML -->
<script src="/pos/static/js/catalog.js" defer></script>
<img src="..." loading="lazy" decoding="async">
3.4 Polling innecesario (🟡 Media)
| Módulo | Intervalo | Problema |
|---|---|---|
| WhatsApp status | 3s | Corre aunque el tab esté oculto |
| WhatsApp mensajes | 5s | Corre aunque el tab esté oculto |
| Auto-print queue | 15s | Siempre activo |
| Dashboard clock | 1s | Actualiza reloj cada segundo |
Fix: Pausar polling cuando document.hidden:
function createVisiblePoller(fn, intervalMs) {
return setInterval(() => { if (!document.hidden) fn(); }, intervalMs);
}
4. Infraestructura — Análisis Detallado
4.1 Nginx — Configuración mínima
Archivo: nginx/nexus-pos.conf
Problemas encontrados:
- ❌ Sin
gzip on - ❌ Sin
expiresheaders para assets estáticos - ❌ Sin
client_body_buffer_size/proxy_buffer_sizetuning - ❌ Proxy timeouts default (60s) — podrían ser más agresivos para requests lentos
- ❌ Sin rate limiting general (solo en login)
Fix recomendado:
# Dentro de cada server block
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json
application/javascript application/rss+xml
application/atom+xml image/svg+xml;
# Cache assets estáticos
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 6M;
add_header Cache-Control "public, immutable";
}
# Proxy tuning
proxy_connect_timeout 10s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
4.2 Gunicorn — Sync workers
Archivo: pos/gunicorn.conf.py
workers = multiprocessing.cpu_count() * 2 + 1 # ~15 workers
worker_class = "sync"
timeout = 120
Con sync workers, cada worker maneja exactamente 1 request a la vez. Si un request al catálogo tarda 3 segundos (por DB lenta), ese worker está bloqueado.
15 workers × 1 request = 15 requests concurrentes máximo.
Fix: Migrar a gevent o gthread:
worker_class = "gthread"
threads = 4
workers = 4 # Menos workers, más threads por worker
Esto permite que un worker maneje múltiples requests concurrentes sin bloquearse en I/O de DB.
4.3 Redis — Subutilizado (🟠 Alta)
Métrica actual: 840 KB utilizados
Redis solo se usa para:
- Cache de tasas de cambio (
currency.py) - Cache de stock (
redis_stock_cache.py)
NO se usa para:
- Cache de queries de catálogo (la más costosa)
- Cache de clasificación Nexpart (
_classify_cachees in-memory por proceso) - Cache de búsquedas
- Sesiones compartidas entre workers
Fix: Mover _classify_cache (Nexpart classification) a Redis:
# En catalog_service.py
import json
import redis
r = redis.from_url(os.environ.get('REDIS_URL', 'redis://localhost:6379/0'))
def _classify_cache_get(mye_id):
val = r.get(f"nexus:classify:{mye_id}")
return json.loads(val) if val else None
def _classify_cache_set(mye_id, data):
r.setex(f"nexus:classify:{mye_id}", 300, json.dumps(data))
Beneficio: cache compartido entre los ~15 workers de gunicorn, no 15 cachés separados.
4.4 Flask — Sincrónico
Todo el backend es síncrono (Flask sync + psycopg2 sync). Las queries a vehicle_parts bloquean el thread del worker.
Consideración a futuro: Para endpoints de catálogo, evaluar migrar a un worker queue (Celery + Redis) para queries pesadas, o usar asyncpg + Quart/FastAPI para endpoints específicos.
5. Backend Python — Análisis Detallado
5.1 Cache por proceso vs. compartida
El cache de clasificación Nexpart (_CLASSIFY_CACHE) vive en memoria de cada proceso de gunicorn. Con 15 workers:
- Cada worker tiene su propio cache de 500 entradas máximo
- Un
mye_idcacheado en worker A no está disponible en worker B - Cache hit rate teórico máximo: ~6.7% (1/15) si la carga se distribuye uniformemente
Fix: Redis, como se describe en 4.3.
5.2 Serialización JSON ineficiente
Flask jsonify() usa el encoder estándar de Python. Para respuestas grandes (listas de 30 parts con múltiples campos), esto puede ser lento.
Fix: Considerar orjson como encoder de Flask:
from flask import Flask
import orjson
class OrjsonProvider:
def dumps(self, obj, **kwargs):
return orjson.dumps(obj).decode('utf-8')
def loads(self, s, **kwargs):
return orjson.loads(s)
app = Flask(__name__)
app.json = OrjsonProvider()
orjson es 2–10× más rápido que json estándar.
5.3 Respuestas API sin paginación en algunos endpoints
Algunos endpoints retornan arrays completos sin límite:
get_categories()puede retornar todas las categorías (aceptable)get_stock_bulk()escanea TODOinventory_operations- Historial de movimientos sin paginación en
inventory_bp
Verificar: Que ningún endpoint retorne datasets sin bounds.
6. Roadmap de Optimización Priorizado
Fase 1 — Quick Wins (1–2 días, alto impacto)
| # | Tarea | Archivos | Impacto estimado |
|---|---|---|---|
| 1.1 | Agregar gzip en nginx | nginx/nexus-pos.conf |
-40–60% transfer size |
| 1.2 | Agregar defer a scripts |
Templates HTML | -200–500ms TTI |
| 1.3 | Reemplazar innerHTML += por map+join |
catalog.js, dashboard.js |
Elimina reflow storm |
| 1.4 | Event delegation en breadcrumb/cart | catalog.js |
Elimina memory leak |
| 1.5 | AbortController en API calls | catalog.js, inventory.js, pos.js |
Elimina race conditions |
| 1.6 | Agregar índice parcial warehouse_inventory(part_id) WHERE stock_quantity > 0 |
SQL | Elimina seq scan |
| 1.7 | Cache de years/brands en sessionStorage |
catalog.js |
-2 API calls/página |
Fase 2 — DB Performance (3–5 días, impacto masivo)
| # | Tarea | Archivos | Impacto estimado |
|---|---|---|---|
| 2.1 | Implementar connection pooling | tenant_db.py, dashboard/auth.py |
-30–50% latencia, sin connection errors |
| 2.2 | Crear tabla inventory_stock_summary con triggers |
SQL + inventory_engine.py |
Stock: O(n) → O(1) |
| 2.3 | Precalcular part_vehicle_preview materialized view |
SQL | Búsqueda: -500ms–2s |
| 2.4 | N+1 fix en process_sale |
pos_engine.py |
-20 queries por venta |
| 2.5 | Optimizar CTE stock_per_oem |
catalog_service.py |
Elimina full scan |
| 2.6 | Agregar índices faltantes críticos | SQL | Mejora plan de queries |
Fase 3 — Cache & Infra (5–7 días)
| # | Tarea | Archivos | Impacto estimado |
|---|---|---|---|
| 3.1 | Mover _classify_cache a Redis |
catalog_service.py |
Cache hit: 6% → 80%+ |
| 3.2 | Cachear queries de catálogo en Redis | catalog_bp.py, catalog_service.py |
-300ms–1s por query |
| 3.3 | Migrar gunicorn a gthread |
gunicorn.conf.py |
4× concurrency |
| 3.4 | Agregar loading="lazy" a imágenes |
catalog.js |
-60% image bytes inicial |
| 3.5 | Extraer CSS inline a archivos compartidos | Todos los .html |
-60–80% payload HTML |
| 3.6 | Minificar JS/CSS en deploy | CI/CD script | -40–60% transfer |
Fase 4 — Arquitectura (2–4 semanas, inversión mayor)
| # | Tarea | Archivos | Impacto estimado |
|---|---|---|---|
| 4.1 | Particionar vehicle_parts |
SQL | Escalabilidad ilimitada |
| 4.2 | Evaluar asyncpg + Quart para endpoints de catálogo |
Blueprints | I/O no bloqueante |
| 4.3 | Migrar serialización a orjson |
app.py |
2–10× faster JSON |
| 4.4 | Implementar virtual scroll en tablas grandes | inventory.js, dashboard.js |
Smooth 1000+ rows |
| 4.5 | Worker queue (Celery) para tareas pesadas | Nuevo módulo | No bloquear requests HTTP |
7. Métricas de éxito propuestas
Después de implementar Fase 1 + Fase 2, el sistema debería alcanzar:
| Métrica | Antes | Objetivo | Cómo medir |
|---|---|---|---|
| Latencia catálogo Local (p95) | 500ms–3s | < 200ms | Gunicorn access logs |
| Latencia búsqueda (p95) | 500ms–2s | < 150ms | Gunicorn access logs |
| Tiempo First Paint | ~1.5s | < 800ms | Lighthouse |
| Transferencia JS por página | 526 KB | < 200 KB | DevTools Network |
| Conexiones DB por request | 1–2 nuevas | 0 (pool) | pg_stat_activity |
| Redis hit rate | < 1% | > 70% | INFO stats |
| Memory leak (30 min browsing) | +50–100 MB | < +10 MB | Chrome DevTools |
8. Riesgos y Consideraciones
| Riesgo | Mitigación |
|---|---|
Particionar vehicle_parts es complejo y riesgoso |
Hacer en staging primero; usar pg_partman; mantener backup |
Connection pooling requiere manejar putconn en todos los blueprints |
Refactorizar tenant_db.py para usar context managers (with get_conn():) |
Migrar a gthread puede exponer race conditions en código no thread-safe |
Pruebas exhaustivas; gthread es más seguro que gevent para código sync existente |
| Cache Redis agrega punto de falla | Siempre tener fallback a DB directo; Redis no es crítico para operación |
inventory_stock_summary requiere triggers correctos |
Tests de integración para todas las operaciones de stock |
Anexo: Implementación FASE 7 (2026-04-27)
Todas las fases 1–3 del roadmap de optimización fueron implementadas en commits 175dda6 a f893391.
Checklist de fixes aplicados
| # | Tarea | Estado | Commit |
|---|---|---|---|
| 1.1 | gzip en nginx | ✅ | 175dda6 |
| 1.2 | defer en scripts |
✅ | 175dda6 |
| 1.3 | innerHTML += → map+join |
✅ | 175dda6 |
| 1.4 | Event delegation en cart | ✅ | 175dda6 |
| 1.5 | AbortController en API calls | ✅ | 175dda6 |
| 1.6 | Índice parcial warehouse_inventory |
✅ | e3c85fd |
| 1.7 | sessionStorage cache | ✅ | 175dda6 |
| 2.1 | Connection pooling | ✅ | e3c85fd |
| 2.2 | inventory_stock_summary + triggers |
✅ | e3c85fd |
| 2.4 | N+1 fix en process_sale |
✅ | e3c85fd |
| 2.6 | Índices faltantes críticos | ✅ | e3c85fd |
| 3.1 | _classify_cache → Redis |
✅ | e21722a |
| 3.2 | Vehicle info cache en Redis | ✅ | e21722a |
| 3.3 | Gunicorn gthread |
✅ | e21722a |
| 3.4 | loading="lazy" en imágenes |
✅ | 21959f1 |
| 3.5 | Extraer CSS inline | ✅ | f893391 |
| 3.6 | Minificar JS/CSS | ✅ | 21959f1 + f893391 |
Items residuales NO abordados en FASE 7
| Item | Ubicación | Riesgo |
|---|---|---|
| Breadcrumb aún usa listeners por elemento (no event delegation) | catalog.js:275–285 |
🟡 Memory leak residual |
Cache-busting ?t=Date.now() en imágenes de inventario |
inventory.js:552 |
🟡 Anula cache browser |
Polling sin document.hidden (WhatsApp, clock, auto-print) |
Múltiples JS | 🟡 CPU innecesario en tabs ocultos |
CTE stock_per_oem full scan |
catalog_service.py:986–991 |
🟠 Seq scan warehouse_inventory |
Materialized view part_vehicle_preview |
— | 🟠 Reemplazada por Redis cache; MV creada en Opción C |
Métricas estimadas post-FASE 7
| Métrica | Antes | Después FASE 7 |
|---|---|---|
| Latencia catálogo Local (p95) | 500ms–3s | ~150–400ms |
| Transferencia JS por página | 526 KB | ~180 KB (gzip + minify) |
| Transferencia CSS por página | ~247 KB inline | ~25 KB cached (external) |
| Conexiones DB por request | 1–2 nuevas | 0 (pool) |
| Redis hit rate | < 1% | ~80%+ |
| Stock lookup | O(n) operaciones | O(1) summary table |
Documento generado automáticamente a partir del análisis de codebase, métricas de sistema y mejores prácticas de performance. Actualizado 2026-04-27 para reflejar implementación FASE 7.