From n00b to ZeroCool / Profesionalización
Performance frontend: cuando tu web pesa más que Doom
Baja el peso de tu frontend: bundles, imágenes, Core Web Vitals y medición con fixes reales sin romper tu deploy.
Lo que vale la pena leer aquí
En tu compu del jale “carga bien”. En tu iPhone nuevo “más o menos”. Pero en un Android gama media (el de tu cliente, tu tía, medio México) se siente como si la página estuviera pensando en su vida.
Intro con gancho
Son las 10:17 pm. Vas en el Uber, abres tu sitio con datos porque el Wi‑Fi del depa anda en modo “se cayó otra vez”… y la pantalla se queda en blanco.
En tu compu del jale “carga bien”. En tu iPhone nuevo “más o menos”. Pero en un Android gama media (el de tu cliente, tu tía, medio México) se siente como si la página estuviera pensando en su vida.
Y llega el mensaje que pica:
“Oye, tu landing pesa un chingo, ¿no?”
Sí. Tu web pesa más que Doom.
No por mala fe: el stack moderno te deja meter dependencias como si fueran stickers. Una UI library completa “por si acaso”, dos analytics encimados, icons inline para todo, imágenes 4K para un hero que se ve de 320px, y un bundle que ya pide su propio sprint.
Qué vas a aprender
- Medir performance sin autoengañarte (Lighthouse, WebPageTest, DevTools) y qué números sí importan.
- Encontrar qué está inflando tu bundle y qué cortar sin romper production.
- Optimizar imágenes de verdad (AVIF/WebP,
srcset, tamaños, lazy loading). - Atacar Core Web Vitals con cambios que se sienten: LCP, INP, CLS.
- Un checklist de release para que no vuelvas a subir un “Doom bundle” por accidente.
Contexto práctico: por qué pasa (y por qué pega duro en MX)
En un equipo real, el performance se muere por mil microdecisiones:
- “Metemos este paquete porque ya lo usamos en otro proyecto.”
- “Se ve más pro con esta animación.”
- “El diseño pide video de fondo.”
- “Marketing subió el banner del Buen Fin directo al CMS, sin compresión.”
Y acá hay fricción extra:
- Mucha banda navega con prepago o planes limitados. Cada MB duele.
- La gama media manda. Si tu app depende de CPU para parsear/ejecutar un JS enorme, no es edge case: es la mayoría.
Performance no es lujo. Es conversión, menos tickets de “se queda cargando” y menos rollback a las 2 am.
Paso a paso: rescate de una web que pesa más que Doom
Regla de oro: mide primero, cambia después, vuelve a medir. Si no, te vas a ir por la tangente, el deadline te va a comer y vas a terminar optimizando lo que ni dolía.
1) Mide como si fueras tu usuario (no tu Mac)
Herramientas rápidas:
- Chrome DevTools: Performance, Network, Coverage
- Lighthouse (incógnito, sin extensiones)
- WebPageTest (simula dispositivos y redes de verdad)
Setup recomendado (sin drama):
- Abre DevTools → Network.
- Activa:
- Throttling: Fast 3G (o Slow 4G si te quieres ver benevolente).
- Disable cache.
- Recarga.
- Observa:
- Tamaño total transferido.
- Request count.
- El archivo JS más pesado.
- Cuándo aparece el primer contenido que parece “la página”.
Qué números sí importan (modo “no me vendas humo”):
- LCP (Largest Contentful Paint): objetivo < 2.5s
- INP (Interaction to Next Paint): objetivo < 200ms
- CLS (Cumulative Layout Shift): objetivo < 0.1
Tip de guerra: Lighthouse puede darte un score decente y aun así sentirse lento. WebPageTest con filmstrip + waterfall te pone la realidad enfrente.
2) Encuentra al culpable: tu bundle
Si tu JS pesa demasiado, el problema no es solo “descarga”, también es:
- download
- parse
- compile
- execute
En un Android promedio eso se traduce en: pantalla congelada, taps que no agarran, scroll trabado. El usuario no dice “tu main thread está ocupada”, dice “tu página está bien lenta” y se va.
Con Vite (ejemplo):
npm i -D rollup-plugin-visualizer
vite.config.js:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
react(),
visualizer({
open: true,
filename: 'dist/stats.html',
gzipSize: true,
brotliSize: true
})
]
})
Build:
npm run build
Abre dist/stats.html y pregúntate sin piedad:
- ¿Por qué tengo moment.js completo si solo formateo una fecha?
- ¿Por qué estoy importando una UI library entera por un modal?
- ¿Por qué metí lodash completo en vez de imports específicos?
Decisión práctica:
- Si una dependencia aporta 200–400KB (o más) y solo la usas en una pantalla… huele a lazy loading o a replace.
3) Code splitting: carga lo que se usa, cuando se usa
No todo tiene que llegar en el primer request. Lo “admin”, lo “settings”, el editor WYSIWYG, el mapa, la parte de reportes… casi siempre puede esperar.
React + lazy (ejemplo):
import { lazy, Suspense } from 'react'
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'))
export function Routes() {
return (
<Suspense fallback={<div>Cargando…</div>}>
<AdminDashboard />
</Suspense>
)
}
Tradeoff real:
- Pro: baja el bundle inicial (mejor LCP/INP).
- Con: metes loading states y más requests. Si tu CDN está flojo o la red está chafa, se nota.
Tip de oficina: si tu app vive detrás de VPN corporativo que se cae, cuida que lo crítico sea mínimo y sólido. Lo “nice to have” que se cargue después.
4) Quita JS innecesario (la dieta más efectiva)
Antes de micro-optimizar, corta grasa. La neta: casi siempre ahí está el 80/20.
Cosas que suelen sobrar en producción:
- Trackers duplicados (GA + GTM + pixel + “otro más” porque marketing lo pidió en corto).
- Librerías de animación para un efecto mínimo.
- Polyfills globales que ya no aplican.
DevTools → Coverage
- DevTools → More tools → Coverage
- Start instrumenting coverage
- Recarga
Vas a ver qué porcentaje de JS/CSS ni se toca. Si hay archivos con 60–80% unused en rutas críticas, ahí está tu señal.
Decisión práctica:
- Si es una librería: busca alternativa más pequeña o impórtala por partes.
- Si es tu código: divide por rutas/features y deja de mandar todo al primer paint.

5) Imágenes: el “Doom” silencioso
Muchos sitios pesados no mueren por JS. Mueren por imágenes mal tratadas.
La escena típica: un freelance lo cotizó barato, se fue a la talacha de copiar/pegar un template, subió el hero a 4000px “para que no se vea pixeleado”… y en production cada visita paga la factura.
Checklist que sí jala:
- Convierte a AVIF y deja fallback a WebP/JPEG.
- Sirve tamaños correctos con
srcsetysizes. - Declara
widthyheight(para evitar CLS). - Lazy load lo que no está arriba del fold.
Ejemplo con <picture>:
<picture>
<source type="image/avif" srcset="/img/hero-640.avif 640w, /img/hero-1280.avif 1280w">
<source type="image/webp" srcset="/img/hero-640.webp 640w, /img/hero-1280.webp 1280w">
<img
src="/img/hero-1280.jpg"
alt="Producto"
width="1280"
height="720"
loading="eager"
fetchpriority="high"
/>
</picture>
Regla de batalla:
- Lo del hero (probable LCP) va eager y con prioridad.
- Lo demás:
loading="lazy".
Herramientas rápidas:
- Squoosh (para probar sin instalar nada)
- Sharp/imagemin en tu pipeline
- CDN con image resizing (Cloudflare Images, Imgix, etc.) cuando el proyecto lo justifica
6) Ataca Core Web Vitals con acciones específicas
LCP: que el contenido principal aparezca rápido
Causas comunes:
- Imagen hero enorme sin optimizar
- CSS bloqueante
- Fonts pesadas
- JS que bloquea el main thread
Acciones:
- Preload del recurso LCP (si aplica):
<link rel="preload" as="image" href="/img/hero-1280.webp" imagesrcset="/img/hero-640.webp 640w, /img/hero-1280.webp 1280w" imagesizes="100vw">
- CSS crítico (o mínimo: no cargues 3 CSS gigantes antes del primer paint).
- Reduce JS inicial con splitting.
INP: que la página responda cuando la tocas
INP sufre cuando:
- tienes handlers pesados
- renderizas listas enormes
- haces trabajo denso en el main thread (parsing, JSON gigante, re-render masivo)
Acciones:
- Debounce/throttle en inputs:
function debounce(fn, wait = 200) {
let t
return (...args) => {
clearTimeout(t)
t = setTimeout(() => fn(...args), wait)
}
}
- Virtualiza listas (React Window, etc.) cuando hay cientos/miles.
- Mueve trabajo a Web Worker si estás calculando cosas densas.
CLS: que no brinque todo como pinball
CLS típico:
- imágenes sin dimensiones
- banners que aparecen arriba
- fuentes que cambian layout
Acciones:
- Reserva espacio (width/height o CSS
aspect-ratio):
.card-media {
aspect-ratio: 16 / 9;
width: 100%;
}
- Si marketing exige banner arriba, deja el slot reservado desde el inicio. Si lo insertas después, empujas todo y el usuario siente el golpe.
7) Fonts: el lujo caro
Si cargas 4 weights + italics “para verse premium”, estás pagando con LCP/CLS.
Acciones rápidas:
- Limita weights (regular + bold suele bastar).
- Usa
font-display: swap. - Subset si puedes.
Ejemplo:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-subset.woff2') format('woff2');
font-display: swap;
}
8) Caching y compresión: el clásico que se olvida
Si tu server no manda gzip/brotli, estás dejando performance gratis en la mesa.
Revisa en Network → Response Headers:
content-encoding: br(ideal) ogzipcache-controlpara assets con hash (max-age=31536000, immutable)
Decisión práctica:
- Con assets con hash (
app.8c12f3.js): cachea agresivo sin miedo. - Sin hash: cache fuerte = receta para “yo sigo viendo la versión vieja” y el típico “ya le di hard refresh” en soporte.
Screenshots sugeridos
- DevTools Network con throttling activado y “Disable cache”.
- Lighthouse report mostrando LCP/INP/CLS.
- WebPageTest waterfall + filmstrip.
- Coverage report con porcentaje de JS/CSS no usado.
- Visualizer (treemap) del bundle mostrando dependencias pesadas.
Errores comunes + solución
-
Error: “Optimicé todo y no cambió nada.”
Solución: Estabas arreglando lo que no era el bottleneck. Vuelve a medir y enfócate en el recurso LCP y el JS más pesado. -
Error: Lazy loading en el hero (LCP).
Solución: Hero =loading="eager"+ prioridad + tamaño correcto. -
Error: Importar librerías completas (
import _ from 'lodash').
Solución: Importa por función o usa alternativas nativas; valida el tree-shaking real de tu bundler. -
Error: Meter un carousel “ligerito” que trae 200KB.
Solución: ¿Sube conversión o es capricho? Si no hay métrica, es deuda. -
Error: Fonts con 6 weights + italics y sin
swap.
Solución: Reduce pesos, subset yfont-display: swap. -
Error: CLS por banners sorpresa (Buen Fin, Hot Sale, “últimas horas”).
Solución: Reserva el espacio desde el inicio o colócalo sin empujar contenido.

Checklist final (antes de hacer deploy)
- Medí en Fast 3G/Slow 4G con cache desactivado.
- Identifiqué el recurso LCP y lo optimicé (peso, preload, prioridad).
- Revisé el treemap del bundle y corté dependencias grandes o las moví a lazy.
- Usé Coverage y bajé el % de código no usado en rutas críticas.
- Imágenes: AVIF/WebP,
srcset/sizes, dimensiones definidas, lazy donde toca. - CLS controlado: espacios reservados para media/banners.
- Fonts: pocos weights,
swap, woff2. - Assets con hash + cache-control correcto.
- Compresión (brotli/gzip) activa en producción.
- Volví a medir y guardé evidencia (capturas o link de WebPageTest) en el pull request.
FAQ
1) ¿Qué pesa más: JS o imágenes?
Depende. En muchos proyectos reales las imágenes ganan por goleada. En apps tipo dashboard, el JS suele ser el villano principal.
2) ¿Lighthouse alcanza para decidir?
Sirve para el primer golpe, pero no te cases. WebPageTest + waterfall te enseña qué bloquea qué, y eso guía decisiones.
3) ¿Tree-shaking siempre funciona?
No siempre. Si la librería no está bien empaquetada (ESM) o importas de forma que rompe el tree-shaking, te llevas módulos enteros sin querer.
4) ¿Lazy loading siempre mejora?
Mejora el arranque, pero puede empeorar la experiencia si lo usas en contenido que el usuario necesita ya. Úsalo con criterio: rutas y features no críticas.
5) ¿Cómo evito que se rompa otra vez en 2 sprints?
Pon guardrails: presupuesto de bundle, revisión en PR, medición en CI aunque sea básica, y evidencia. Si no vive en el workflow, la urgencia lo aplasta.
Siguiente episodio
Ya carga rápido… hasta que llegan los trackers, el A/B testing y el “solo un script más”. Toca pelearse con third‑parties: cómo medirlos, aislarlos y que no secuestren tu INP.
Idea para cerrar bien este post: toma una sola práctica de aquí y conviértela en algo que tu equipo pueda aplicar hoy.
Cuando un artículo aterriza en decisiones reales, deja de ser contenido y se vuelve ventaja.


