FASE 7: Quick Wins de Performance — Optimización Fase 1

Cambios implementados:

1. Nginx:
   - gzip on (compresión JS/CSS/JSON)
   - Cache headers para assets estáticos (6M)
   - Proxy buffer tuning (10s connect, 30s read)

2. Frontend catalog.js:
   - Reemplazados 8x innerHTML += en loops por map+join
   - Event delegation en breadcrumb y cart (elimina memory leak)
   - AbortController en apiFetch (evita race conditions)
   - sessionStorage cache para years-all y brands por modo

3. Frontend templates HTML:
   - defer en todos los scripts POS (mejora First Paint)

4. Dashboard JS:
   - innerHTML += fix en dashboard.js y cuentas.js

5. Base de datos:
   - Índice parcial idx_wi_part_stock_positive en warehouse_inventory

6. Documentación:
   - docs/performance_audit_2026.md con análisis completo y roadmap

Tests: 73/73 pasando (compat + fase3 + fase5 + fase6)
This commit is contained in:
2026-04-27 07:19:37 +00:00
parent efbd763e43
commit 175dda6212
19 changed files with 813 additions and 146 deletions

View File

@@ -186,8 +186,20 @@
var cartItems = JSON.parse(localStorage.getItem('pos_cart') || '[]');
// ─── API helper ───
var currentAbort = null;
function apiFetch(url) {
return fetch(url, { headers: headers })
// Cancel previous navigation/search GETs to avoid race conditions
if (currentAbort) {
currentAbort.abort();
currentAbort = null;
}
var opts = { headers: headers };
if (url.indexOf('/pos/api/') === 0 && url.indexOf('mode=') !== -1 || url.indexOf('/years') !== -1 || url.indexOf('/brands') !== -1 || url.indexOf('/models') !== -1 || url.indexOf('/engines') !== -1 || url.indexOf('/categories') !== -1 || url.indexOf('/groups') !== -1 || url.indexOf('/part-types') !== -1 || url.indexOf('/parts') !== -1 || url.indexOf('/search') !== -1) {
currentAbort = new AbortController();
opts.signal = currentAbort.signal;
}
return fetch(url, opts)
.then(function (resp) {
if (resp.status === 401) {
localStorage.removeItem('pos_token');
@@ -197,6 +209,7 @@
return resp.json();
})
.catch(function (e) {
if (e.name === 'AbortError') return null;
console.error('API error:', e);
return null;
});
@@ -333,8 +346,18 @@
setupLevelFilter(true);
showLoading();
var cacheKey = 'nexus:brands:' + catalogMode;
var cached = sessionStorage.getItem(cacheKey);
if (cached) {
hideLoading();
var data = JSON.parse(cached);
renderBrands(data);
return;
}
apiFetch(API + '/brands?mode=' + catalogMode).then(function (data) {
hideLoading();
if (data && data.data) sessionStorage.setItem(cacheKey, JSON.stringify(data));
if (!data || !data.data || !data.data.length) {
if (!data) {
enterOfflineMode();
@@ -359,6 +382,30 @@
});
}
function renderBrands(data) {
if (!data || !data.data || !data.data.length) {
if (!data) {
enterOfflineMode();
return;
}
showEmpty('Sin marcas', 'El catalogo no tiene marcas con partes disponibles.');
return;
}
navGrid.className = 'nav-grid';
navGrid.innerHTML = data.data.map(function (b) {
return '<div class="nav-card" role="listitem" data-brand-id="' + b.id_brand + '" data-name="' + esc(b.name_brand) + '">' +
'<div class="nav-card__name">' + esc(b.name_brand) + '</div>' +
'</div>';
}).join('');
navGrid.querySelectorAll('.nav-card').forEach(function (card) {
card.addEventListener('click', function () {
nav.brand = { id: parseInt(this.dataset.brandId), name: this.dataset.name };
loadModels();
});
});
}
function loadModels() {
nav.level = 'models';
pushNavState();
@@ -1463,18 +1510,19 @@
cartTaxEl.textContent = '$' + fmt(tax);
cartTotalEl.textContent = '$' + fmt(subtotal + tax);
// Wire cart buttons
cartItemsEl.querySelectorAll('[data-cart-action]').forEach(function (btn) {
btn.addEventListener('click', function () {
var idx = parseInt(this.dataset.idx);
var action = this.dataset.cartAction;
if (action === 'dec') updateQuantity(idx, cartItems[idx].quantity - 1);
else if (action === 'inc') updateQuantity(idx, cartItems[idx].quantity + 1);
else if (action === 'remove') removeFromCart(idx);
});
});
}
// Event delegation for cart buttons (attached once)
cartItemsEl.addEventListener('click', function (e) {
var btn = e.target.closest('[data-cart-action]');
if (!btn) return;
var idx = parseInt(btn.dataset.idx);
var action = btn.dataset.cartAction;
if (action === 'dec') updateQuantity(idx, cartItems[idx].quantity - 1);
else if (action === 'inc') updateQuantity(idx, cartItems[idx].quantity + 1);
else if (action === 'remove') removeFromCart(idx);
});
function toggleCart() {
var isOpen = cartSidebar.classList.toggle('open');
cartOverlay.classList.toggle('open', isOpen);
@@ -1565,6 +1613,22 @@
// Load years on init
function vsLoadYears() {
var cacheKey = 'nexus:years-all';
var cached = sessionStorage.getItem(cacheKey);
if (cached) {
var data = JSON.parse(cached);
var years = data.data || data || [];
if (!years.length) {
years = [];
for (var y = 2026; y >= 1990; y--) years.push({ id_year: y, year_car: y });
}
vsYear.innerHTML = '<option value="">Año...</option>' +
years.map(function (y) {
return '<option value="' + y.id_year + '">' + y.year_car + '</option>';
}).join('');
return;
}
apiFetch(API + '/years-all').then(function (data) {
if (!data) return;
var years = data.data || data;
@@ -1573,16 +1637,19 @@
years = [];
for (var y = 2026; y >= 1990; y--) years.push({ id_year: y, year_car: y });
}
vsYear.innerHTML = '<option value="">Año...</option>';
years.forEach(function (y) {
vsYear.innerHTML += '<option value="' + y.id_year + '">' + y.year_car + '</option>';
});
sessionStorage.setItem(cacheKey, JSON.stringify(data));
vsYear.innerHTML = '<option value="">Año...</option>' +
years.map(function (y) {
return '<option value="' + y.id_year + '">' + y.year_car + '</option>';
}).join('');
}).catch(function () {
// Fallback: generate years statically
vsYear.innerHTML = '<option value="">Año...</option>';
for (var y = 2026; y >= 1990; y--) {
vsYear.innerHTML += '<option value="' + y + '">' + y + '</option>';
}
var fallbackYears = [];
for (var y = 2026; y >= 1990; y--) fallbackYears.push(y);
vsYear.innerHTML = '<option value="">Año...</option>' +
fallbackYears.map(function (y) {
return '<option value="' + y + '">' + y + '</option>';
}).join('');
});
}
@@ -1603,10 +1670,10 @@
apiFetch(API + '/brands?year_id=' + yearId + '&mode=' + catalogMode).then(function (data) {
var brands = data.data || data;
if (!brands) return;
vsBrand.innerHTML = '<option value="">Marca...</option>';
brands.forEach(function (b) {
vsBrand.innerHTML += '<option value="' + b.id_brand + '">' + esc(b.name_brand) + '</option>';
});
vsBrand.innerHTML = '<option value="">Marca...</option>' +
brands.map(function (b) {
return '<option value="' + b.id_brand + '">' + esc(b.name_brand) + '</option>';
}).join('');
});
}
@@ -1625,10 +1692,10 @@
apiFetch(API + '/models?brand_id=' + brandId + (yearId ? '&year_id=' + yearId : '')).then(function (data) {
var models = data.data || data;
if (!models) return;
vsModel.innerHTML = '<option value="">Modelo...</option>';
models.forEach(function (m) {
vsModel.innerHTML += '<option value="' + m.id_model + '">' + esc(m.display_name || m.name_model) + '</option>';
});
vsModel.innerHTML = '<option value="">Modelo...</option>' +
models.map(function (m) {
return '<option value="' + m.id_model + '">' + esc(m.display_name || m.name_model) + '</option>';
}).join('');
});
}
@@ -1644,11 +1711,11 @@
apiFetch(API + '/engines?model_id=' + modelId + '&year_id=' + yearVal).then(function (data) {
var engines = data.data || data;
if (!engines) return;
vsEngine.innerHTML = '<option value="">Motor...</option>';
engines.forEach(function (e) {
var label = e.name_engine + (e.trim_level ? ' (' + e.trim_level + ')' : '');
vsEngine.innerHTML += '<option value="' + e.id_mye + '">' + esc(label) + '</option>';
});
vsEngine.innerHTML = '<option value="">Motor...</option>' +
engines.map(function (e) {
var label = e.name_engine + (e.trim_level ? ' (' + e.trim_level + ')' : '');
return '<option value="' + e.id_mye + '">' + esc(label) + '</option>';
}).join('');
// If only 1 engine, auto-select
if (engines.length === 1) {
vsEngine.value = engines[0].id_mye;
@@ -1841,10 +1908,10 @@
apiFetch(API + '/brands?year_id=' + match.year_id).then(function (brandData) {
var brands = brandData && (brandData.data || brandData);
if (!brands) return;
vsBrand.innerHTML = '<option value="">Marca...</option>';
brands.forEach(function (b) {
vsBrand.innerHTML += '<option value="' + b.id_brand + '">' + esc(b.name_brand) + '</option>';
});
vsBrand.innerHTML = '<option value="">Marca...</option>' +
brands.map(function (b) {
return '<option value="' + b.id_brand + '">' + esc(b.name_brand) + '</option>';
}).join('');
vsBrand.disabled = false;
vsClear.style.display = '';
@@ -1854,10 +1921,10 @@
apiFetch(API + '/models?brand_id=' + match.brand_id + '&year_id=' + match.year_id).then(function (modelData) {
var models = modelData && (modelData.data || modelData);
if (!models) return;
vsModel.innerHTML = '<option value="">Modelo...</option>';
models.forEach(function (m) {
vsModel.innerHTML += '<option value="' + m.id_model + '">' + esc(m.display_name || m.name_model) + '</option>';
});
vsModel.innerHTML = '<option value="">Modelo...</option>' +
models.map(function (m) {
return '<option value="' + m.id_model + '">' + esc(m.display_name || m.name_model) + '</option>';
}).join('');
vsModel.disabled = false;
if (match.model_id) {
@@ -1866,11 +1933,11 @@
apiFetch(API + '/engines?model_id=' + match.model_id + '&year_id=' + match.year_id).then(function (engData) {
var engines = engData && (engData.data || engData);
if (!engines) return;
vsEngine.innerHTML = '<option value="">Motor...</option>';
engines.forEach(function (e) {
var elabel = e.name_engine + (e.trim_level ? ' (' + e.trim_level + ')' : '');
vsEngine.innerHTML += '<option value="' + e.id_mye + '">' + esc(elabel) + '</option>';
});
vsEngine.innerHTML = '<option value="">Motor...</option>' +
engines.map(function (e) {
var elabel = e.name_engine + (e.trim_level ? ' (' + e.trim_level + ')' : '');
return '<option value="' + e.id_mye + '">' + esc(elabel) + '</option>';
}).join('');
vsEngine.disabled = false;
// Auto-select engine if only one or if match specifies it