Performance y deploy

"Una app de 100ms se siente instantánea. Una de 1 segundo se siente lenta. Una de 3 segundos hace que el usuario se vaya."

Qué vas a aprender en este capítulo

Construir la app es la mitad del trabajo. La otra mitad: que sea rápida, que se vea bien en mobile, que se desplegue automáticamente, y que sepamos cuándo se rompe. Este capítulo cubre performance, deploy y monitoreo.


5.1 Core Web Vitals

📐 Fundamento

Google mide la experiencia de usuario con tres métricas principales:

Métrica Mide Objetivo (bueno)
LCP (Largest Contentful Paint) Tiempo hasta que se renderiza el elemento más grande < 2.5s
INP (Interaction to Next Paint) Latencia de interacciones (clicks, tipeo) < 200ms
CLS (Cumulative Layout Shift) Movimiento visual durante la carga < 0.1

(INP reemplazó a FID — First Input Delay — en marzo 2024.)

Otras métricas importantes:

  • FCP (First Contentful Paint): cuándo aparece el primer texto/imagen.
  • TTFB (Time to First Byte): respuesta del servidor.
  • TBT (Total Blocking Time): tiempo bloqueado por JavaScript.

Medir con Lighthouse:

# CLI
npx lighthouse https://la-esquina.com --view

# O en Chrome DevTools: pestaña Lighthouse → Generate report

Performance score: 0-100. Objetivo: > 90.

Real User Monitoring (RUM) con web-vitals:

import { onCLS, onINP, onLCP, onTTFB } from 'web-vitals';

function enviarMetrica(metric) {
  fetch('/api/metricas', {
    method: 'POST',
    body: JSON.stringify(metric),
    keepalive: true  // continúa enviando aunque el usuario navegue
  });
}

onCLS(enviarMetrica);
onINP(enviarMetrica);
onLCP(enviarMetrica);

5.2 Optimizaciones de frontend

📐 Fundamento

1. Code splitting: dividir el bundle en chunks que se cargan bajo demanda.

// Antes — todo en un bundle
import AdminPanel from './AdminPanel';

// Después — solo se carga cuando se navega a /admin
const AdminPanel = lazy(() => import('./AdminPanel'));

Resultado: el bundle inicial es más pequeño → carga más rápida.

2. Tree shaking: eliminar código no usado en build time.

// MAL — importa la librería completa
import _ from 'lodash';
_.debounce(...)

// BIEN — solo importa lo necesario
import debounce from 'lodash/debounce';
debounce(...)

3. Optimización de imágenes:

<!-- Antes -->
<img src="hero.jpg" />

<!-- Después: WebP/AVIF, lazy loading, responsive -->
<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" 
       loading="lazy"           <!-- carga cuando entra a viewport -->
       width="800" height="400"  <!-- evita layout shift -->
       alt="Pupusas">
</picture>

4. Caché HTTP agresivo para assets estáticos:

# nginx config
location ~* \.(css|js|png|jpg|webp|svg|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

Combinar con content hashing: app-abc123.js cambia cuando el código cambia → URL nueva → invalidación automática.

5. Preload de recursos críticos:

<link rel="preload" href="/fuentes/inter-var.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preconnect" href="https://api.la-esquina.com">

6. Service Worker para offline / PWA:

// sw.js
self.addEventListener('fetch', (e) => {
  e.respondWith(
    caches.match(e.request).then(res => res || fetch(e.request))
  );
});

// En main.js
navigator.serviceWorker.register('/sw.js');

7. Virtualización de listas largas:

Renderizar 10,000 filas en el DOM mata el browser. Solo renderizar las visibles.

import { useVirtualizer } from '@tanstack/react-virtual';

function ListaGrande({ items }) {
  const parentRef = useRef(null);
  const rowVirtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50
  });
  
  return (
    <div ref={parentRef} style={{ height: '500px', overflow: 'auto' }}>
      <div style={{ height: rowVirtualizer.getTotalSize() }}>
        {rowVirtualizer.getVirtualItems().map(row => (
          <div key={row.key} style={{ position: 'absolute', top: row.start }}>
            {items[row.index].nombre}
          </div>
        ))}
      </div>
    </div>
  );
}

5.3 CDN y arquitectura de delivery

💡 Intuición

Tu servidor está en San Salvador. Un usuario en Madrid carga tu sitio: la latencia es ~150ms por request. Con un CDN (Content Delivery Network), el usuario en Madrid recibe los assets desde un servidor en España: ~10ms.

📐 Fundamento

Cómo funciona un CDN:

Usuario en Madrid                          Usuario en San Salvador
       ↓                                              ↓
[CDN edge: España]                          [CDN edge: México]
       ↓ (si no cachea)                              ↓
       └────────→ [Origin: tu servidor en SS] ←──────┘

Qué cachear en CDN:

  • Imágenes, CSS, JS — siempre (con cache hashing).
  • Fonts — siempre (cambian raramente).
  • HTML — solo si es estático (SSG).
  • API responses — solo si son cacheables (públicos, no por usuario).

Cloudflare es el CDN gratuito más usado:

1. Crear cuenta en Cloudflare.
2. Cambiar DNS de tu dominio a los nameservers de Cloudflare.
3. Cloudflare actúa como proxy entre internet y tu servidor.
4. Beneficios automáticos: CDN global, DDoS protection, TLS automático, cache.

Otros CDNs: Fastly, AWS CloudFront, Bunny.net, KeyCDN.

Edge computing: ejecutar código JS en los edges del CDN (cerca del usuario):

// Cloudflare Worker — corre en el edge
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  // Esto corre en el datacenter más cercano al usuario
  // Latencia: pocos ms
  const url = new URL(request.url);
  
  if (url.pathname === '/api/menu') {
    // Cachear el menú por 5 minutos en el edge
    const cached = await caches.default.match(request);
    if (cached) return cached;
    
    const response = await fetch('https://api.la-esquina.com/menu');
    await caches.default.put(request, response.clone());
    return response;
  }
  
  return fetch(request);
}

5.4 Deploy a producción

📐 Fundamento

Stack deploy moderno:

Capa Plataformas
Frontend (SPA/SSG) Vercel, Netlify, Cloudflare Pages, GitHub Pages
Backend (API) Railway, Fly.io, Render, AWS, GCP, DigitalOcean
Base de datos Neon, Supabase, Railway, RDS
Storage de archivos S3, R2 (Cloudflare), Supabase Storage
Background jobs Inngest, Trigger.dev, Celery + Redis

Deploy con Vercel (frontend React):

# Conectar repo de GitHub a Vercel
vercel  # CLI

# vercel.json
{
  "framework": "vite",
  "buildCommand": "npm run build",
  "outputDirectory": "dist",
  "rewrites": [
    { "source": "/api/:path*", "destination": "https://api.la-esquina.com/:path*" }
  ]
}

Push a main → deploy automático en segundos.

Deploy con Docker (backend):

# Dockerfile multistage para backend Python
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH

# Usuario no-root por seguridad
RUN useradd -m appuser && chown -R appuser /app
USER appuser

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
# docker-compose.yml para desarrollo local
version: '3.8'
services:
  backend:
    build: .
    ports: ["8000:8000"]
    environment:
      DATABASE_URL: postgresql://user:pass@db:5432/esquina
      REDIS_URL: redis://redis:6379
    depends_on: [db, redis]
  
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: esquina
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    volumes: [pgdata:/var/lib/postgresql/data]
  
  redis:
    image: redis:7-alpine

volumes:
  pgdata:

Variables de entorno por ambiente:

# .env.development
DATABASE_URL=postgresql://localhost:5432/dev
DEBUG=true

# .env.production
DATABASE_URL=postgresql://prod-host/prod
DEBUG=false
SENTRY_DSN=https://...

Nunca commitear secretos: .env en .gitignore. Usar secret managers en producción (AWS Secrets Manager, HashiCorp Vault, Doppler).

Health checks y zero-downtime deploys:

@app.get("/health")
def health():
    return {"status": "ok", "version": os.environ.get("APP_VERSION", "dev")}

@app.get("/ready")
def ready():
    # Verificar dependencias críticas
    try:
        db.execute("SELECT 1")
        redis.ping()
        return {"status": "ready"}
    except Exception as e:
        return JSONResponse({"status": "not ready", "error": str(e)}, status_code=503)

Kubernetes / load balancer usa estos endpoints para hacer rolling updates sin downtime.


5.5 Monitoreo y observabilidad

📐 Fundamento

Tres pilares (recordatorio del capítulo 4 de SO):

  1. Logs — qué pasó. Sentry para errores, Logtail/Datadog para todo.
  2. Métricas — cuánto. Prometheus + Grafana o Datadog.
  3. Trazas — dónde tardó. Jaeger, Zipkin, OpenTelemetry.

Sentry para frontend y backend:

// React
import * as Sentry from "@sentry/react";

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  environment: import.meta.env.MODE,
  tracesSampleRate: 0.1,  // 10% de transactions
  replaysSessionSampleRate: 0.01  // 1% de sesiones grabadas para replay
});

// Capturar errores manualmente
try {
  hacerAlgo();
} catch (e) {
  Sentry.captureException(e);
}
# FastAPI
import sentry_sdk
from sentry_sdk.integrations.fastapi import FastApiIntegration

sentry_sdk.init(
    dsn=os.environ['SENTRY_DSN'],
    integrations=[FastApiIntegration()],
    traces_sample_rate=0.1,
    environment=os.environ['ENV']
)

Métricas custom (Prometheus):

from prometheus_client import Counter, Histogram, generate_latest

pedidos_creados = Counter('pedidos_creados_total', 'Total de pedidos creados', ['local'])
duracion_request = Histogram('http_request_duration_seconds', 'Duración', ['endpoint'])

@app.post("/pedidos")
def crear(pedido):
    with duracion_request.labels('crear_pedido').time():
        nuevo = db.crear(pedido)
        pedidos_creados.labels(pedido.local).inc()
        return nuevo

@app.get("/metrics")
def metrics():
    return Response(generate_latest(), media_type="text/plain")

Alertas:

# Alertmanager / Grafana
- alert: HighErrorRate
  expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05
  for: 5m
  annotations:
    summary: "Error rate > 5% por 5 minutos"
    
- alert: HighLatency
  expr: histogram_quantile(0.95, http_request_duration_seconds) > 2
  for: 10m
  annotations:
    summary: "P95 latency > 2s"

🛠️ En la práctica

La Esquina App: arquitectura de deploy completa

┌─────────────────────────────────────────────────────────┐
│                     Usuarios                            │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
              ┌──────────────────────┐
              │  Cloudflare CDN+WAF  │  ← cache, DDoS, TLS
              └──────────────────────┘
                          │
        ┌─────────────────┼─────────────────┐
        ▼                                   ▼
  ┌──────────────┐                   ┌──────────────┐
  │  Frontend    │                   │  Backend API │
  │  Vercel      │                   │  Railway     │
  │  React+Vite  │                   │  FastAPI     │
  │  app.la-     │                   │  api.la-     │
  │  esquina.com │                   │  esquina.com │
  └──────────────┘                   └──────────────┘
                                            │
                                ┌───────────┴───────────┐
                                ▼                       ▼
                        ┌─────────────┐         ┌──────────────┐
                        │ PostgreSQL  │         │   Redis      │
                        │ (Neon)      │         │ (Upstash)    │
                        └─────────────┘         └──────────────┘
                                │
                                ▼
                        ┌─────────────┐
                        │  Sentry     │  ← errores
                        │  Posthog    │  ← analytics
                        └─────────────┘

CI/CD:
- Push a main → GitHub Actions corre tests
- Tests pasan → Vercel + Railway despliegan automáticamente
- Vercel preview deployments para cada PR

Costos estimados (escala pequeña):
- Vercel: free tier (Hobby)
- Railway: $5/mes
- Neon Postgres: free tier
- Upstash Redis: free tier
- Cloudflare: free tier
- Sentry: free tier (5K errors/mes)

Total: ~$5/mes para empezar.

5.6 Ejercicios

✏️ Ejercicio 5.1 — Auditoría de performance

Tu sitio La Esquina App tiene estos resultados de Lighthouse:

  • LCP: 4.2s (objetivo < 2.5s)
  • INP: 350ms (objetivo < 200ms)
  • CLS: 0.25 (objetivo < 0.1)
  • Performance score: 52/100
  • Bundle JS: 1.2MB

Diagnosticá las causas probables y propone 5 optimizaciones concretas en orden de prioridad.


5.7 Cierre del libro

Este libro recorrió el panorama del desarrollo web moderno:

  1. APIs REST — el modelo dominante para APIs.
  2. GraphQL — alternativa flexible para UIs complejas.
  3. Frontend moderno con React — SPAs con hooks y TanStack Query.
  4. Tiempo real con WebSockets — colaboración y notificaciones live.
  5. Performance y deploy — hacer que sea rápido y llevarlo a producción.

La web evoluciona constantemente. Las herramientas cambiarán, pero los principios — performance, accesibilidad, seguridad, observabilidad — son los que perduran.


5.8 Para profindizar

5.X Mini-proyecto integrador

🏗️ Proyecto final — Una app web full-stack en producción

Alcance: construir y desplegar una app web completa que use todo el libro: API REST, GraphQL, frontend moderno, tiempo real y observabilidad. El producto no es lo importante — es la stack.

Idea sugerida: un mini-Twitter para tu carrera (post, follow, feed en tiempo real). O un sistema de turnos online de la pupusería con notificaciones live.

Entregables (1 monorepo):

  1. Backend (cap. 1) — FastAPI con al menos 5 endpoints REST documentados con OpenAPI.
  2. GraphQL (cap. 2) — un endpoint /graphql para queries complejas (feed paginado con filtros).
  3. Frontend (cap. 3) — React/Vue/Svelte. Routing, estado global, formularios.
  4. Tiempo real (cap. 4) — WebSocket para notificaciones live de nuevos posts/turnos.
  5. Performance (cap. 5) — Lighthouse score > 90 en mobile, code splitting, lazy loading de rutas.
  6. Deploy (cap. 5) — Vercel/Railway/Fly.io. Health check /health, métricas básicas, Sentry para errores.
  7. CI mínimo: lint + tests + build en cada PR.

Criterio de éxito: podés mandar el link a un amigo, lo abre desde su celular, crea una cuenta y publica un post — todo en menos de 30 segundos.

Tiempo estimado: 4-6 semanas, ideal en pareja. Es el proyecto bisagra para conseguir trabajo de fullstack.


Definiciones nuevas: Core Web Vitals, LCP, INP, CLS, Lighthouse, Real User Monitoring (RUM), code splitting, tree shaking, lazy loading, Service Worker, PWA, virtualización, CDN, edge computing, Cloudflare Workers, deploy continuo, health check, observabilidad, Sentry, Prometheus, alertas.