# 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:** ```sql 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_id` o `model_year_engine_id` - Índices ocupan más espacio que la tabla (indicativo de índices anchos o muchos índices secundarios) - El índice `idx_vehicle_parts_part` probablemente se usa para queries que filtran por `part_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` ```python 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`: ```python 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` ```sql 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:** ```sql -- Í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(...)`: ```sql 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: ```sql 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:** 1. **Corto plazo:** Agregar índice compuesto y filtrar por fecha reciente si el negocio no requiere stock histórico: ```sql CREATE INDEX idx_inv_ops_inventory_branch_created ON inventory_operations(inventory_id, branch_id, created_at DESC); ``` 2. **Mediano plazo:** Mantener una tabla de `inventory_stock_summary` actualizada por triggers: ```sql 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_operations` que actualiza el sumario en tiempo real. Las lecturas pasan de `O(n operaciones)` a `O(1)`. --- ### 2.5 `smart_search()` — DISTINCT ON sobre 2B filas (🔴 Crítico) **Archivo:** `pos/services/catalog_service.py:1339-1349` ```sql 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: ```sql 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` ```python 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: ```python 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. ```javascript years.forEach(function (y) { vsYear.innerHTML += ''; }); ``` Cada iteración fuerza al browser a **reparsear y re-renderizar todo el subtree**. Complejidad O(N²). **Fix:** ```javascript vsYear.innerHTML = '' + years.map(y => ``).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: ```javascript 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:** ```javascript 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):** ```nginx # En nginx/nexus-pos.conf gzip on; gzip_types text/css application/javascript application/json; gzip_min_length 1024; ``` ```html ``` ### 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`: ```javascript 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 `expires` headers para assets estáticos - ❌ Sin `client_body_buffer_size` / `proxy_buffer_size` tuning - ❌ Proxy timeouts default (60s) — podrían ser más agresivos para requests lentos - ❌ Sin rate limiting general (solo en login) **Fix recomendado:** ```nginx # 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` ```python 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`: ```python 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_cache` es in-memory por proceso) - Cache de búsquedas - Sesiones compartidas entre workers **Fix:** Mover `_classify_cache` (Nexpart classification) a Redis: ```python # 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_id` cacheado 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: ```python 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 TODO `inventory_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.*