12.1 📍 Hoy: dónde estamos
Capacidad headroom (Hostinger Business típico)
- Storage: 200 GB → ocupamos ~1% → headroom enorme.
- RAM PHP: 1.5 GB típicamente → suficiente para queries actuales.
- BD: sin límite duro, pero "fair use" sugiere <5 GB.
- Procesos concurrentes: ~25 PHP-FPM workers → suficiente para 5 users.
- Outbound HTTP: ~30 conexiones simultáneas (afecta sync de ChessERP).
12.2 📈 Proyección a 2 años (Mayo 2028)
Asumiendo crecimiento moderado del negocio y uso del sistema:
| Métrica | Hoy (May 26) | +1 año | +2 años | Notas |
|---|---|---|---|---|
Filas ventas |
515 K | ~830 K | ~1.2 M | +20% crecimiento anual |
Filas caja_raw_egresos_detalle |
262 K | ~420 K | ~600 K | Crece con volumen operativo |
| BD total | ~900 MB | ~1.6 GB | ~2.4 GB | Dentro de Hostinger |
| SKUs catálogo | 7.870 | ~9.000 | ~10.500 | Crecimiento lento |
| Clientes activos | ~3.000 | ~4.500 | ~6.000 | Si Zonas Áridas abre sucursales |
| Users concurrentes | ~5 | ~15 | ~30 | Si más áreas usan el sistema |
| Períodos cargados | 22 | 34 | 46 | Lineal |
| P95 dashboard (sin acción) | ~2 s | ~3.5 s | ~6 s ⚠️ | Degradación lineal sin MVs |
| P95 dashboard (con MVs) | ~50 ms | ~80 ms | ~120 ms | Estable con vistas materializadas |
Cuándo cada componente "se rompe" sin optimizar
- BD < 3 GB → MariaDB sigue rápida con índices apropiados.
- BD entre 3-10 GB → empezar a particionar tablas históricas.
- BD > 10 GB → considerar migrar a VPS con MySQL/Postgres dedicado.
- 5 → 50 users concurrentes → Hostinger todavía aguanta con cache HTTP.
- 50+ users concurrentes → migrar a VPS con Nginx + PHP-FPM tuned.
- Dashboards > 5s consistentes → MVs son obligatorias.
12.3 🚧 Cuellos de botella identificados
| # | Componente | Cuando se siente | Mitigación |
|---|---|---|---|
| 1 | data_api.php · ~20 queries por dashboard |
Cuando ventas > 1M filas | Vistas materializadas + cache HTTP (F2-1, F2-3) |
| 2 | JOIN ventas × articulos_raw con CAST |
Si se quita CAST + se agrega índice, OK por 5 años | PERF-03 + PERF-02 |
| 3 | Sync de ChessERP HTTP largas | Ya hoy: meses pesados pueden timeout | Hostinger workers límite + retry logic (RES-01) |
| 4 | Parseo Excel en cliente (SheetJS) | Si archivo > 50 MB browser puede colgarse | Mover parseo a backend (P-CAJ-2) |
| 5 | dashboard.php inyecta datos en HTML |
Cuando Cliente/Artículo > 5000 filas | Lazy load por tab via fetch (PERF-06) |
| 6 | Sin pool de conexiones BD | Con > 20 users concurrentes | PHP-FPM tuning o migrar a Nginx/ProxySQL |
| 7 | BD compartida con sitio público | Picos de tráfico web compiten | Separar instancias (Fase 2+) |
| 8 | UPDATE en cada hit de listas/index.php |
YA hoy desperdicia recursos | Trigger o cron (P-LIS-2) |
| 9 | Pipeline 3-tier corre en cliente | Archivos > 20 MB ya son lentos | Mover al backend (P-CAJ-2) |
| 10 | Sin CDN para Tabulator/Chart.js | Cada hit baja ~700 KB de librerías | Self-host con headers Cache-Control |
12.4 🗄️ Estrategias para escalar la BD
Nivel 1 · Índices (gratis, inmediato)
-- PERF-02: índice compuesto
ALTER TABLE ventas ADD INDEX idx_sync_anulado_articulo
(sync_id, anulado, dsArticulo(60));
-- Para drill-downs
ALTER TABLE ventas ADD INDEX idx_sync_supervisor (sync_id, dsSupervisor);
ALTER TABLE ventas ADD INDEX idx_sync_cliente (sync_id, idCliente);
ALTER TABLE ventas ADD INDEX idx_sync_proveedor (sync_id, proveedor(100));
-- Para caja/finanzas
ALTER TABLE caja_raw_gastos_tipificados ADD INDEX idx_anio_rubro
((YEAR(fecha_comp)), rubro_id); -- functional index MariaDB 10.6+
Nivel 2 · Vistas materializadas (cron diario)
Recalcular cada mañana → todo el día queda servido desde la MV (50 ms vs 2 s).
CREATE TABLE ventas_mensual_supervisor_mv (
sync_id INT,
dsSupervisor VARCHAR(100),
ventas_netas DECIMAL(15,2),
ventas_reales DECIMAL(15,2),
costo_total DECIMAL(15,2),
contribucion DECIMAL(15,2),
bultos_total DECIMAL(15,2),
peso_kg_total DECIMAL(15,2),
clientes_unicos INT,
total_comprobantes INT,
refreshed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (sync_id, dsSupervisor)
);
CREATE TABLE ventas_mensual_supervisor_mv … (idem para vendedor, depósito, cliente, artículo, proveedor);
-- Refresh nightly:
TRUNCATE ventas_mensual_supervisor_mv;
INSERT INTO ventas_mensual_supervisor_mv ...;
Nivel 3 · Particionado por sync_id
-- Cuando ventas supere 2M filas
ALTER TABLE ventas
PARTITION BY RANGE (sync_id) (
PARTITION p2024 VALUES LESS THAN (15), -- syncs id 1-14
PARTITION p2025 VALUES LESS THAN (28),
PARTITION p2026 VALUES LESS THAN (50),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
-- Beneficio: queries con WHERE sync_id IN (...) sólo tocan particiones relevantes
-- DROP de un sync borra una partición (O(1)) en vez de DELETE WHERE
Nivel 4 · Archivado de períodos viejos
-- Mover ventas de más de 3 años a tabla archivo
CREATE TABLE ventas_archivo LIKE ventas;
ENGINE = Archive; -- MariaDB · ~80% menos espacio, solo INSERT/SELECT
INSERT INTO ventas_archivo SELECT * FROM ventas
WHERE sync_id IN (SELECT id FROM sincronizaciones WHERE anio < YEAR(NOW()) - 3);
DELETE FROM ventas WHERE sync_id IN (...);
-- Tabla "ventas" queda ágil; lecturas de archivo via UNION
Nivel 5 · Réplica de lectura (cuando > 50 users)
┌──────────────┐
│ App (PHP) │
└──────┬───────┘
│
├──── INSERT/UPDATE ────► ┌─────────────────┐
│ │ MariaDB MASTER │
│ │ (escrituras) │
│ └─────────────────┘
│ │
│ │ async replication
│ ▼
└──── SELECT ───────────► ┌─────────────────┐
│ MariaDB SLAVE │
│ (sólo lecturas) │
└─────────────────┘
Nivel 6 · Migrar a Postgres + Citus / TimescaleDB (long-term)
Si se llega a >100M filas o >100 users → Postgres con extensiones de time-series.
12.5 ⚙️ Estrategias para escalar la app
1 · Opcode cache (gratis)
; php.ini
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 1
opcache.revalidate_freq = 60
opcache.preload = /home/u120688891/public_html/reportes/_shared/preload.php
; Verificar:
opcache_get_status();
Mejora del orden 30-40% en tiempo de respuesta sin tocar código.
2 · HTTP Cache + ETag
Ver §11 F2-3. Reduce carga al backend en 50-80% para endpoints JSON.
3 · Pool de PDO singleton
Ya existe en db_reportes.php. Asegurar que se usa en TODOS los archivos
(algunos crean PDO ad-hoc — ver reset_db.php).
4 · Lazy loading de tabs
// dashboard.php · cada tab carga sus datos AL hacer click por primera vez
const tabState = { vend: 'pending', sup: 'pending', dep: 'pending',
cli: 'pending', art: 'pending', prov: 'pending' };
function loadTabIfNeeded(tabKey) {
if (tabState[tabKey] !== 'pending') return;
tabState[tabKey] = 'loading';
fetch(`/reportes/venta/api/tab.php?tab=${tabKey}&sync=${syncId}&sucursal=${sucursal}`)
.then(r => r.json())
.then(data => {
initTabulator(tabKey, data);
tabState[tabKey] = 'loaded';
});
}
// Reduce el HTML inicial de 2 MB a ~300 KB
// Mejor First Contentful Paint (FCP) en mobile
5 · Workers para tareas pesadas
El sync de ChessERP bloquea la UI. Mover a background:
// /reportes/_shared/queue.php (file-based simple)
function queue_push(string $job, array $payload): int {
$id = uniqid();
file_put_contents(__DIR__ . "/jobs/$id.json", json_encode([
'job' => $job, 'payload' => $payload,
'created_at' => date('c'),
]));
return $id;
}
// Cron cada 1 min procesa los jobs:
* * * * * php /reportes/_shared/queue_worker.php
// UI hace polling al endpoint de status del job
GET /reportes/_shared/job_status.php?id=abc123
→ {status: 'running', progress: 67, message: 'Lote 5 de 8'}
12.6 🎨 Estrategias para escalar el frontend
1 · Self-host de librerías CDN
/reportes/_shared/vendor/
├── tabulator/
│ ├── tabulator.min.js (~250 KB)
│ └── tabulator.min.css (~80 KB)
├── chartjs/
│ └── chart.umd.js (~210 KB)
└── sheetjs/
└── xlsx.full.min.js (~900 KB)
// Servir con Cache-Control: public, max-age=31536000, immutable
// Browsers cachean para siempre → reduce hits a CDN externo
2 · Modularización del JS
El dashboard.php de venta tiene ~2000 líneas de JS inline. Extraer a módulos ES6:
/reportes/venta/assets/js/
├── app.js ← bootstrap general (existe)
├── formatters.js ← fmtMoney, fmtPct, fmtTons
├── tabs.js ← openTab, fullscreen
├── rent-tabs.js ← inicialización de los 6 Tabulator
├── charts.js ← Chart.js wrappers
└── filters.js ← cross-filter modal
// dashboard.php
<script type="module" src="assets/js/dashboard-main.js"></script>
3 · Virtual DOM-free para listas grandes
Tabulator ya usa virtual scroll, pero asegurar que virtualDom: true esté activado en todas las tablas.
4 · Service Worker para offline-friendly
// /reportes/sw.js
self.addEventListener('fetch', e => {
const url = new URL(e.request.url);
// Cache librerías estáticas
if (url.pathname.startsWith('/reportes/_shared/vendor/')) {
e.respondWith(
caches.open('vendor-v1').then(cache =>
cache.match(e.request).then(r => r || fetch(e.request).then(resp => {
cache.put(e.request, resp.clone());
return resp;
}))
)
);
}
});
5 · Imágenes (logos) optimizadas
Los SVG ya son ligeros. Si se agregan fotos / banners → WebP + lazy loading.
12.7 ☁️ Cuándo migrar de Hostinger
Triggers para migrar a VPS
| Trigger | Cuándo | Tipo recomendado |
|---|---|---|
| BD > 5 GB | Estimado año 3 | VPS con 4-8 GB RAM (DigitalOcean / Hetzner / Contabo) |
| P95 > 5s consistente | Si no se aplican MVs | VPS para tuning fino de MySQL my.cnf |
| > 30 users concurrentes | Año 2 | VPS + Nginx + PHP-FPM dedicado |
| Requiere cron sub-minute | Para alertas en tiempo real | VPS (Hostinger limita crones a cada 5min) |
| Necesita Redis / queue | Cuando hay background jobs serios | VPS con Redis instalado |
| Compliance (SOC2, ISO) | Si Zonas Áridas crece a corporativo | Cloud certificado (AWS, GCP, Azure) |
Costo estimado por escenario
| Escenario | Hosting | USD/mes | Cuándo aplica |
|---|---|---|---|
| Actual | Hostinger Business shared | 5 | Hoy |
| Mid-tier | Hostinger VPS 2 GB | 12 | Año 1-2 |
| Profesional | Hetzner CX31 (2 vCPU, 8 GB) | 11 | Año 2-3 |
| Empresa pequeña | DigitalOcean Droplet 4 GB + Managed MySQL | 40 | Año 3+ |
| Híbrido | VPS app + DBaaS (PlanetScale / Neon) | 50-100 | Si se llega a > 100 users |
| Cloud completo | AWS / GCP con RDS + ECS | 200+ | Sólo si compliance lo exige |
Recomendación: empezar a planificar la migración a VPS cuando se cumpla cualquiera de:
- BD > 3 GB
- P95 > 4s y no se quieren implementar MVs
- Se necesitan crones < 5 min (alertas tiempo real)
- Se quiere Redis/Memcached
12.8 👥 Escalar el equipo
Etapas según tamaño del equipo
1-2 devs (hoy)
- Stack simple (PHP + JS vanilla) bien justificado.
- Documentación markdown extensa (~4.700 líneas).
- Releases con semver real, changelog inline.
- Manual: branches por feature, merge a main, deploy manual.
3-5 devs (próximos 12 meses)
- Code review obligatorio (PRs en GitHub/GitLab).
- Linter + formatter en pre-commit (PHP-CS-Fixer + Prettier).
- CI básico: PHPUnit + PHPStan en cada PR.
- Convención de commits (Conventional Commits).
- Documentation as Code: este HTML versionado + ADRs.
5-10 devs (año 2)
- Squads por dominio (venta, caja, cashflow, plataforma).
- Tech Lead dedicado.
- RFCs para cambios mayores.
- OnCall rotation para incidentes.
- SLA interno: uptime, P95, time-to-fix-bug.
12.9 🗺️ Roadmap consolidado de escalado
AÑO 1 (Mayo 2026 → Mayo 2027)
──────────────────────────────
Q3 2026 ► Fase 0 Hardening (SEC-01/02/03) ★ Obligatorio
Eliminar código muerto · Audit log
Q3 2026 ► Fase 1 Plataforma compartida
_shared/, .env, fórmulas en clase
CAJERO_PROV a tabla DB
Q4 2026 ► Fase 2.A · Vistas materializadas
ventas_mensual_supervisor_mv y similares
Cron diario de refresh
Q4 2026 ► Fase 2.B · Cron syncs automáticos
Sync nocturno venta · cashflow alerts
Q1 2027 ► Fase 2.C · Cache HTTP + Lazy loading
ETag en endpoints JSON
Lazy load de tabs en dashboard
AÑO 2 (Mayo 2027 → Mayo 2028)
──────────────────────────────
Q2 2027 ► Fase 3.A · Forecast cashflow + Alertas
Q3 2027 ► Fase 3.B · Anomaly detection + Recomendador límites
Q3 2027 ► Migración a VPS (Hetzner) + opcache.preload
Q4 2027 ► Fase 3.C · API pública + Power BI / Looker
Q1 2028 ► Particionado de ventas + Archivado >3 años
Q1 2028 ► Reporte ejecutivo PDF mensual automatizado
AÑO 3 (Mayo 2028 → Mayo 2029)
──────────────────────────────
Q2 2028 ► Réplica de lectura · ProxySQL
Q3 2028 ► Multi-empresa: separar tenants
Q4 2028 ► Mobile app (PWA con offline-first)
Q1 2029 ► ML predictivo: stock óptimo, recomendador SKUs
KPIs del propio sistema a trackear
- P95 de cada dashboard (objetivo: <1.5s año 1, <500ms año 2).
- Uptime mensual (objetivo: 99.5% año 1, 99.9% año 2).
- Cobertura de tests (objetivo: 40% año 1, 70% año 2).
- Tiempo MTTR de incidentes (objetivo: <4h año 1, <1h año 2).
- NPS interno (encuesta semestral a los usuarios).