Programación Web · Capítulo 18
Optimización y Performance Web: Velocidad que Importa
Aprende a medir y mejorar la velocidad de tu aplicación web, porque cada segundo de carga perdido cuesta usuarios y dinero.
1. Por Qué el Performance Importa
Google encontró que el 53% de los usuarios abandona un sitio móvil si tarda más de 3 segundos en cargar. Amazon calculó que cada 100ms de latencia adicional cuesta 1% de ventas. La velocidad no es solo una preferencia — afecta directamente el posicionamiento en buscadores (SEO) y los ingresos.
2. Core Web Vitals: Las Métricas que Google Mide
Google usa tres métricas principales para evaluar la experiencia del usuario y afectan el ranking en búsquedas:
LCP (Largest Contentful Paint): tiempo hasta que el elemento más grande es visible. Meta: < 2.5s
FID (First Input Delay): tiempo hasta que la página responde al primer clic. Meta: < 100ms
CLS (Cumulative Layout Shift): cuánto se mueven los elementos mientras carga. Meta: < 0.1
| Métrica | Bueno | Necesita mejora | Malo |
| LCP | < 2.5s | 2.5s – 4s | > 4s |
| FID | < 100ms | 100ms – 300ms | > 300ms |
| CLS | < 0.1 | 0.1 – 0.25 | > 0.25 |
3. Herramientas de Medición
- Lighthouse (en Chrome DevTools → pestaña Lighthouse): auditoría completa de performance, accesibilidad, SEO y más.
- PageSpeed Insights (pagespeed.web.dev): análisis con datos reales de Chrome.
- WebPageTest (webpagetest.org): pruebas desde diferentes ubicaciones y conexiones.
- Chrome DevTools → Network: ver exactamente qué archivos se cargan y cuánto tardan.
- Chrome DevTools → Performance: grabar y analizar el runtime del browser.
4. Optimización de Imágenes
Las imágenes representan típicamente el 60-70% del peso de una página web. Optimizarlas tiene el mayor impacto.
<!-- Formato moderno WebP (30-50% más pequeño que JPEG) con fallback -->
<picture>
<source srcset="hero.webp" type="image/webp">
<source srcset="hero.avif" type="image/avif">
<img src="hero.jpg" alt="Imagen principal" width="1200" height="600">
</picture>
<!-- Lazy loading: solo cargar cuando está cerca del viewport -->
<img src="foto.webp" alt="Foto" loading="lazy" decoding="async">
<!-- Responsive images: diferente tamaño según la pantalla -->
<img
src="imagen-800.webp"
srcset="imagen-400.webp 400w,
imagen-800.webp 800w,
imagen-1200.webp 1200w"
sizes="(max-width: 640px) 400px,
(max-width: 1024px) 800px,
1200px"
alt="Imagen responsiva"
loading="lazy"
>
<!-- Siempre especificar width y height para evitar CLS -->
<img src="logo.png" width="200" height="60" alt="Logo">
5. Optimización de JavaScript
Code Splitting con React
// Sin code splitting: TODO el JS se carga al inicio
import PaginaAdmin from "./PaginaAdmin"; // Cargado siempre
import PaginaReportes from "./PaginaReportes"; // Cargado siempre
// Con code splitting: solo se carga cuando el usuario navega allí
import { lazy, Suspense } from "react";
const PaginaAdmin = lazy(() => import("./PaginaAdmin"));
const PaginaReportes = lazy(() => import("./PaginaReportes"));
function App() {
return (
<Suspense fallback={<div>Cargando...</div>}>
<Routes>
<Route path="/admin" element={<PaginaAdmin />} />
<Route path="/reportes" element={<PaginaReportes />} />
</Routes>
</Suspense>
);
}
// El bundle inicial puede reducirse un 50-80%
defer y async en Scripts
<!-- Sin atributo: bloquea el parser HTML hasta descargar y ejecutar -->
<script src="app.js"></script>
<!-- async: descarga en paralelo, ejecuta tan pronto como descarga -->
<!-- Para scripts independientes (analytics, publicidad) -->
<script async src="analytics.js"></script>
<!-- defer: descarga en paralelo, ejecuta DESPUÉS del HTML completo -->
<!-- Para scripts que necesitan el DOM listo -->
<script defer src="app.js"></script>
6. Estrategias de Caché
// Express: configurar headers de caché
// Activos estáticos (CSS, JS, imágenes con hash en el nombre)
// El hash en el nombre garantiza que si cambia el archivo, cambia la URL
app.use("/static", express.static("public", {
maxAge: "1y", // Caché por 1 año — el hash asegura invalidación
immutable: true
}));
// API: no cachear respuestas dinámicas
app.get("/api/datos", (req, res) => {
res.set("Cache-Control", "no-store");
res.json(datos);
});
// HTML: revalidar en cada visita
app.get("/", (req, res) => {
res.set("Cache-Control", "no-cache"); // Revalida antes de usar caché
res.sendFile("index.html");
});
// Vite genera nombres con hash automáticamente:
// app.js → app.a1b2c3d4.js
// styles.css → styles.e5f6g7h8.css
// Esto permite cache-busting automático:
// - Si el archivo NO cambió → misma URL → se sirve desde caché
// - Si el archivo SÍ cambió → URL diferente → se descarga nuevo
7. Optimización de CSS
/* Critical CSS: estilos para lo visible "above the fold"
Inlinar en el HTML para evitar bloqueo de renderizado */
/* En el <head> del HTML: */
<style>
/* Solo estilos del header, hero y lo visible sin scroll */
body { font-family: sans-serif; margin: 0; }
.hero { background: #0f172a; color: white; padding: 60px; }
</style>
/* El resto del CSS carga de forma no bloqueante: */
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
// Tailwind CSS elimina el CSS no usado automáticamente (PurgeCSS):
// tailwind.config.js
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
// Tailwind escanea estos archivos y elimina clases no usadas
// Un proyecto típico pasa de 3MB a menos de 20KB de CSS
8. Prefetch y Preload
<head>
<!-- Preload: recurso crítico que necesitarás PRONTO en esta página -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero.webp" as="image">
<!-- Prefetch: recurso que probablemente necesitarás en la PRÓXIMA página -->
<link rel="prefetch" href="/dashboard.js">
<!-- Preconnect: establecer conexión TCP/TLS temprana con dominio externo -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://api.ejemplo.com">
</head>
9. Optimización de Consultas a Base de Datos
// ❌ Problema N+1: una consulta por cada usuario
const usuarios = await db.query("SELECT * FROM usuarios");
for (const usuario of usuarios) {
// Esto hace 1 + N consultas (N = número de usuarios)
usuario.pedidos = await db.query(
"SELECT * FROM pedidos WHERE usuario_id = ?",
[usuario.id]
);
}
// ✅ Solución: JOIN en una sola consulta
const usuariosConPedidos = await db.query(`
SELECT u.*, p.id as pedido_id, p.total, p.fecha
FROM usuarios u
LEFT JOIN pedidos p ON u.id = p.usuario_id
`);
// O usando carga eager con un ORM como Prisma:
const usuarios = await prisma.usuario.findMany({
include: { pedidos: true } // Un solo query optimizado
});
Resumen del Capítulo
- Los Core Web Vitals (LCP, FID, CLS) son las métricas de Google que afectan el SEO; mídelos con Lighthouse o PageSpeed Insights.
- Las imágenes son el mayor peso de las páginas; usa WebP/AVIF, lazy loading, srcset responsivo y siempre especifica width/height.
- El code splitting con React.lazy() divide el bundle y solo carga el código cuando el usuario lo necesita, reduciendo el tiempo de carga inicial.
- La caché es la optimización más poderosa; usa nombres con hash para activos estáticos y headers Cache-Control correctos en el servidor.
defer en scripts evita que bloqueen el renderizado del HTML; preload descarga recursos críticos anticipadamente.
- Elimina CSS no utilizado (PurgeCSS/Tailwind) e inlina el CSS crítico en el HTML para evitar bloqueos de renderizado.
- El problema N+1 en consultas de base de datos es una causa frecuente de lentitud; resuélvelo con JOINs o carga eager en el ORM.