Hace poco me puse a auditar mi portafolio (efrenmunoz.cl) con Lighthouse en serio, no solo para sacar un número bonito sino para entender qué estaba pasando de verdad. Performance venía en 83. Terminé en 100. Pero lo interesante no fue subir el número — fue lo que encontré en el camino. Tres bugs concretos que no son obvios a simple vista, y que probablemente tengas en tu propio proyecto si usas React con code-splitting o Helmet.
1. El CLS estaba escondido en un Suspense vacío
Cumulative Layout Shift (CLS) mide cuánto "salta" tu página mientras carga. El mío estaba en 0.343 — bastante mal (Google considera "bueno" cualquier valor bajo 0.1).
Lo raro es que Lighthouse me apuntaba al <footer> como el elemento que más se movía. Pero el footer no tenía animaciones ni nada raro. El problema estaba más arriba, en cómo cargaba cada ruta:
const Loader = () => null;
<Suspense fallback={<Loader />}>
<Home />
</Suspense>
Ese fallback={<Loader />} renderiza literalmente nada mientras el chunk de la página se descarga. Como el layout usa flex-col con el <main> en flex-1, en teoría debería estirarse para llenar la pantalla igual... pero el contenido real de una página como Home es mucho más alto que un viewport. Entonces pasaba esto:
- Primer render: casi no hay contenido → el footer queda pegado casi arriba.
- El chunk de la página carga (rápido, ~100-200ms, pero después del primer paint).
- El contenido real se monta → la página crece varios largos de pantalla de golpe → el footer salta hacia abajo bruscamente.
Ese salto grande, aunque dure milisegundos, cuenta completo para el CLS. La solución fue ridículamente simple una vez que entendí la causa:
const Loader = () => <div className="min-h-screen" />;
Con eso, el placeholder ya reserva una pantalla completa de alto desde el primer render. CLS bajó de 0.343 a 0.082, y después a 0 puro en la mayoría de las páginas.
2. React Helmet no limpia las meta tags que no puso él
Para que WhatsApp y LinkedIn generaran una vista previa decente al compartir el link (imagen, título, descripción), agregué meta tags de Open Graph directo en el index.html estático — porque esos bots no ejecutan JavaScript, así que nunca ven lo que React Helmet inyecta dinámicamente.
Lo que no consideré: cuando la app carga y Helmet mete sus propias tags (og:title, og:description, etc.), no borra las que ya estaban ahí. Terminé con dos <meta name="description"> en cada página — una estática, una dinámica — y encima con valores distintos. Un crawler que sí ejecuta JS (Google, Ahrefs) veía ambas, sin saber cuál priorizar.
La solución: un script inline en el <head>, antes de que React cargue, que borra las tags estáticas apenas hay JavaScript disponible:
<meta data-fallback="true" name="description" content="..." />
<!-- ...resto de tags og:*/twitter:* con data-fallback="true" -->
<script>
document.querySelectorAll('meta[data-fallback="true"]').forEach(el => el.remove());
</script>
Bots sin JS (WhatsApp, LinkedIn) siguen viendo las tags estáticas normal. Cualquiera que sí ejecute JS las ve desaparecer antes de que Helmet monte las suyas — cero duplicados, sin tener que elegir entre "buena vista previa" o "meta tags limpias".
3. Un mismo archivo de imagen sirviendo dos propósitos distintos
Tenía imágenes de proyecto que se usaban como thumbnail en una grilla (mostradas a ~400px de ancho) y como imagen completa en una galería/lightbox (potencialmente pantalla completa). Estaba usando el mismo archivo para ambas cosas — lo que significaba servir una imagen de 2500px de ancho para mostrarla a 400px en la grilla. Un desperdicio de casi 90% del peso descargado, solo en esa página.
La solución no fue "achicar la imagen" (eso arruina la galería), sino generar una segunda versión liviana específica para el thumbnail:
magick original.webp -resize 1600x1600\> -quality 82 original-thumb.webp
Y en los datos del proyecto, agregar un campo aparte:
{
image: "/images/proyecto.webp", // galería, tamaño completo
thumb: "/images/proyecto-thumb.webp", // solo para la grilla
}
Cada imagen se sirve en su contexto correcto. La grilla bajó de ~935 KB a menos de 100 KB en total.
Ninguno de estos tres bugs sale bonito en un dashboard — Lighthouse te dice "CLS alto" o "imagen muy grande", pero no siempre te dice por qué. La única forma de encontrarlos fue meterse al DOM real y cuestionar cada suposición sobre cómo React estaba renderizando las cosas.
Si te está pasando algo parecido en tu proyecto, o quieres que revise el tuyo, puedes escribirme desde efrenmunoz.cl/contacto.
Top comments (0)