Tema
Rendimiento, caché y CDN
Un sitio lento pierde visitantes, ventas y posicionamiento en Google. La buena noticia es que la mayoría de las mejoras son configuraciones del servidor que haces una vez: compresión, caché, imágenes optimizadas y una CDN. En esta lección aprenderás a medir y a acelerar cualquier sitio.
Concepto
La velocidad depende de tres cosas:
- Cuánto pesa lo que envías (HTML, CSS, JS, imágenes, fuentes).
- Cuántas veces lo envías (caché: ¿el navegador vuelve a descargar lo que ya tenía?).
- Qué tan lejos está el servidor del usuario (latencia → CDN).
Core Web Vitals
Google mide la experiencia real con tres métricas:
| Métrica | Qué mide | Bueno |
|---|---|---|
| LCP (Largest Contentful Paint) | Cuándo aparece el elemento principal (imagen o título grande) | ≤ 2.5 s |
| INP (Interaction to Next Paint) | Qué tan rápido responde la página a clics y teclas | ≤ 200 ms |
| CLS (Cumulative Layout Shift) | Cuánto "salta" el contenido mientras carga | ≤ 0.1 |
Paso 1: medir antes de optimizar
- Lighthouse: en Chrome, DevTools → pestaña Lighthouse → Analizar. Prueba en modo incógnito.
- PageSpeed Insights (pagespeed.web.dev): Lighthouse + datos reales de usuarios de Chrome.
- WebPageTest (webpagetest.org): pruebas desde distintas ciudades y conexiones, con diagrama de cascada.
- DevTools → Network: activa "Disable cache" y una conexión lenta ("Slow 4G") para ver qué pesa y qué tarda.
bash
# Tiempo hasta el primer byte (TTFB) y total desde la terminal
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s | Total: %{time_total}s | Tamaño: %{size_download} bytes\n" https://tudominio.comMide, cambia una cosa, vuelve a medir
Así sabrás qué mejora funcionó. Guarda una captura del informe inicial: a los clientes les encanta ver el "antes y después".
Paso 2: compresión (gzip y brotli)
Comprimir texto (HTML, CSS, JS, JSON, SVG) reduce su tamaño entre 60% y 80%.
nginx
http {
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types
text/plain text/css text/xml application/json application/javascript
application/xml application/rss+xml image/svg+xml font/ttf font/otf;
}apache
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css text/xml
AddOutputFilterByType DEFLATE application/javascript application/json
AddOutputFilterByType DEFLATE application/xml image/svg+xml
</IfModule>Brotli comprime aún mejor. En Apache: sudo a2enmod brotli. En Nginx requiere un módulo adicional (libnginx-mod-http-brotli-filter y -static en Ubuntu reciente). Si usas Cloudflare, brotli se aplica automáticamente hacia el visitante.
Comprueba: curl -sI -H "Accept-Encoding: gzip, br" https://tudominio.com | grep -i content-encoding.
No comprimas imágenes JPG/PNG/WebP
Ya están comprimidas; volver a comprimirlas gasta CPU sin beneficio.
Paso 3: caché del navegador
La cabecera Cache-Control le dice al navegador cuánto tiempo puede reutilizar un archivo sin volver a pedirlo.
La estrategia profesional:
| Tipo de archivo | Cache-Control | Por qué |
|---|---|---|
Archivos con hash en el nombre (app.3f9a1c.js) | public, max-age=31536000, immutable | Si el contenido cambia, cambia el nombre. Se pueden cachear un año. |
| Imágenes y fuentes | public, max-age=2592000 (30 días) o más | Cambian poco. |
| HTML | no-cache | El navegador revalida siempre para ver la versión nueva. |
Vite, VitePress, Next.js y Astro ya generan nombres con hash en /assets/ o /_next/static/.
nginx
# Dentro del server block
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location ~* \.(png|jpe?g|gif|webp|avif|svg|ico|woff2?)$ {
expires 30d;
add_header Cache-Control "public";
}
location ~* \.html$ {
add_header Cache-Control "no-cache";
}Paso 4: imágenes
Las imágenes suelen ser el 50% o más del peso de una página.
- Formato moderno: WebP (soporte universal) o AVIF (aún más ligero).
- Tamaño correcto: no envíes una foto de 4000 px para mostrarla a 400 px.
- Carga diferida:
loading="lazy"en las imágenes que están más abajo. - Dimensiones explícitas:
widthyheightevitan saltos (CLS). - La imagen principal (LCP) no lleva lazy; dale prioridad con
fetchpriority="high".
html
<!-- Imagen principal del hero: carga prioritaria -->
<img src="/img/hero-1200.webp" width="1200" height="600"
alt="Interior de la cafetería" fetchpriority="high">
<!-- Imagen responsiva con varios tamaños y formatos -->
<picture>
<source type="image/avif"
srcset="/img/menu-400.avif 400w, /img/menu-800.avif 800w"
sizes="(max-width: 600px) 100vw, 50vw">
<img src="/img/menu-800.webp"
srcset="/img/menu-400.webp 400w, /img/menu-800.webp 800w"
sizes="(max-width: 600px) 100vw, 50vw"
width="800" height="533" loading="lazy" alt="Platillos del menú">
</picture>Convertir en lote desde la terminal:
bash
sudo apt install webp -y
# Convertir todos los JPG de una carpeta a WebP con calidad 80
for f in img/*.jpg; do cwebp -q 80 "$f" -o "${f%.jpg}.webp"; doneHerramientas visuales: Squoosh (squoosh.app) y TinyPNG.
Paso 5: HTTP/2 y HTTP/3
HTTP/2 permite descargar muchos archivos en paralelo por una sola conexión. Se activa junto con HTTPS:
nginx
listen 443 ssl http2;HTTP/3 (sobre QUIC/UDP) mejora aún más en redes móviles; la forma más sencilla de tenerlo es a través de Cloudflare.
Paso 6: caché del lado del servidor
Cuando el cuello de botella es el TTFB (el servidor tarda en generar la página):
- OPcache para PHP (ver LAMP y LEMP): imprescindible, acelera PHP varias veces.
- Caché de página completa: WordPress con plugins de caché; Laravel con
config:cache,route:cacheyCache::remember(). - Redis para guardar resultados de consultas pesadas o sesiones.
- FastCGI cache / proxy_cache de Nginx: guarda la respuesta HTML ya generada.
Ejemplo de micro-caché en Nginx para una app Node (las respuestas se guardan 10 segundos; útil para páginas públicas con mucho tráfico):
nginx
# En http { } de nginx.conf
proxy_cache_path /var/cache/nginx/app levels=1:2 keys_zone=app_cache:10m max_size=500m inactive=60m;
# En el server block
location / {
proxy_cache app_cache;
proxy_cache_valid 200 10s;
proxy_cache_use_stale error timeout updating;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
}No caches páginas privadas
Nunca caches respuestas que dependan del usuario (carrito, panel, perfil). Excluye esas rutas o las peticiones con cookies de sesión.
Paso 7: CDN
Una CDN (Content Delivery Network) guarda copias de tus archivos en servidores repartidos por el mundo. Un usuario en Madrid descarga desde Madrid aunque tu servidor esté en Nueva York.
Ventajas extra: protección DDoS, menos carga en tu servidor, HTTPS y HTTP/3 incluidos.
Cloudflare paso a paso
- Crea una cuenta en Cloudflare y agrega tu sitio (dominio).
- Cloudflare escanea e importa tus registros DNS. Revísalos (especialmente MX y TXT del correo).
- Cambia los nameservers en tu registrador por los dos que te asigna Cloudflare.
- En DNS, la nube naranja (Proxied) significa que el tráfico pasa por la CDN. La gris (DNS only) solo resuelve el nombre.
- Web (
@,www): naranja. - Correo (
mail), FTP, SSH: gris (Cloudflare solo proxea HTTP/HTTPS).
- Web (
- SSL/TLS → Overview → Full (strict). Requiere un certificado válido en tu servidor (Let's Encrypt o un Origin Certificate de Cloudflare).
- SSL/TLS → Edge Certificates: activa Always Use HTTPS.
- Caching: el nivel estándar ya cachea estáticos (CSS, JS, imágenes). Con Cache Rules puedes cachear también HTML de páginas públicas.
- Speed: revisa las optimizaciones disponibles (Brotli, HTTP/3, Early Hints).
- Tras un despliegue, si ves archivos viejos: Caching → Configuration → Purge Cache.
Modo Flexible = problemas
En SSL "Flexible", Cloudflare habla con tu servidor por HTTP. Si tu servidor redirige HTTP→HTTPS, se forma un bucle infinito de redirecciones, y además el tráfico entre Cloudflare y tu servidor va sin cifrar. Usa siempre Full (strict).
IP real del visitante detrás de Cloudflare
Con el proxy activo, tu servidor ve las IPs de Cloudflare. Para registrar la IP real en Nginx:
nginx
# /etc/nginx/conf.d/cloudflare.conf
# Lista oficial y actualizada en: https://www.cloudflare.com/ips/
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;Checklist de rendimiento
- [ ] Compresión gzip/brotli activa
- [ ] Caché larga para archivos con hash; HTML con
no-cache - [ ] Imágenes en WebP/AVIF, con tamaño correcto,
width/heightyloading="lazy" - [ ] Imagen LCP sin lazy y con
fetchpriority="high" - [ ] Fuentes en
woff2, confont-display: swapy solo los pesos necesarios - [ ] JS no crítico con
defer; quitar librerías que no se usan - [ ] HTTP/2 activo con HTTPS
- [ ] OPcache (PHP) o caché de aplicación
- [ ] CDN (Cloudflare) con SSL Full (strict)
- [ ] Lighthouse ≥ 90 en móvil en las páginas principales
Ejercicios
- Escribe la configuración de Nginx para cachear un año
/_next/static/(Next.js). - Una página tiene LCP de 5 s por una imagen JPG de 2.4 MB en el hero. Enumera tres acciones.
- Activaste Cloudflare y el sitio entra en "demasiados redireccionamientos". ¿Qué revisas?
Ver solución
nginx
location /_next/static/ {
proxy_pass http://127.0.0.1:3000;
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}- Convertirla a WebP/AVIF con calidad ~80, redimensionarla al tamaño real de visualización (con
srcsetpara móviles), quitarleloading="lazy"y añadirfetchpriority="high"conwidth/height; además, servirla por CDN. - El modo SSL de Cloudflare: si está en Flexible, cámbialo a Full (strict) (con certificado válido en el servidor).
Resumen
- Mide primero con Lighthouse, PageSpeed Insights o WebPageTest.
- Comprime texto (gzip/brotli) y cachea con estrategia: assets con hash 1 año, HTML
no-cache. - Las imágenes son la mayor ganancia: formato moderno, tamaño correcto, lazy loading.
- OPcache, Redis o caché de Nginx bajan el TTFB.
- Cloudflare como CDN con SSL Full (strict) y DNS del correo en gris.
- Siguiente: Checklist de producción.