# De 89 a 100 en Lighthouse: cómo llevé mi web al rendimiento máximo en una mañana

> Optimización real de un sitio Astro estático: eliminé render-blocking, apliqué el patrón Facade al chatbot y corregí contraste WCAG. De 89/96/92 a 100/100/100 en Lighthouse.

**Autor:** Pedro Luis Cuevas Villarrubia | **Fecha:** 2026-05-10 | **Tags:** rendimiento, Lighthouse, Astro
**URL:** https://asturwebs.es/blog/lighthouse-100-optimizar-rendimiento-web-2026/

---
Esta mañana abrí PageSpeed Insights, escribí asturwebs.es y me encontré con un 89 de rendimiento en móvil. No estaba mal, pero no era suficiente. Dos horas después, tenía un **100/100/100** en rendimiento, accesibilidad y SEO.

No fue magia. Fue método. Te cuento qué hice, por qué funcionó, y qué aprendí.

> **Nota técnica:** Lighthouse mide en "laboratorio" (simula 4G lento, CPU throttled). Para el ranking real, Google usa datos de campo de usuarios reales (CrUX). Un 100 en Lighthouse no garantiza que todos los usuarios vean lo mismo, pero demuestra que el sitio tiene el potencial óptimo. Y eso impacta positivamente en los datos reales.

## El punto de partida

Mi web está construida con **Astro 5** (HTML estático), React para componentes interactivos, y una API en Hono. Nada de WordPress, nada de PHP. A pesar de ser un site estático, Lighthouse me daba:

- **Rendimiento: 89** — FCP 2.6s, LCP 3.2s, Speed Index 2.6s
- **Accesibilidad: 96** — Problemas de contraste
- **SEO: 92** — Enlace poco descriptivo

El diagnóstico fue claro: tres cosas bloqueaban el rendimiento.

## Fix 1: Google Fonts te cuesta 750 milisegundos

Cargar la fuente Inter desde los servidores de Google adds tres requests: DNS, TLS y la descarga del CSS. En 4G lento, eso son **750 ms de render-blocking** — el navegador no pinta nada hasta que resuelve la fuente.

**Solución:** Descargué los archivos woff2 de Inter (latin + latin-ext, solo lo que necesita una web en español) y los serví desde mi propio dominio.

Resultado: 750 ms de bloqueo → 0 ms. Y de paso eliminé dependencia de terceros.

![Lighthouse 90 móvil — punto de partida](/images/90-movil.webp)

## Fix 2: Google Analytics se cargaba DOS veces

Este fue un error mío. Tenía GA en el `<head>` de Layout.astro (para todos los visitantes) Y en el componente CookieConsent (solo si aceptaban cookies). Resultado: 153 KiB de JS innecesario para usuarios que no habían aceptado cookies, y un problema de GDPR.

**Solución:** Eliminé el script de Layout y dejé solo el de CookieConsent, que carga GA únicamente tras el consentimiento.

Ahorro: 153 KiB de JS + 196 ms de hilo principal. Y ahora es legal.

## Fix 3: El ChatWidget mataba el Speed Index

El componente ChatWidget.tsx usa React con `useState`, `useEffect`, `useCallback`... todo eso se hidrataba con `client:load`, lo que significa que el navegador lo descargaba y ejecutaba **antes de pintar la página**.

El chat pesa poco (~5 KiB), pero arrastra React entero (54 KiB). El problema no era el tamaño del archivo — era el **coste de hidratación**: la CPU del móvil pasaba 80ms "desempaquetando" React y evaluando el componente antes de permitir que el usuario hiciera scroll. Esos 80ms de Total Blocking Time (TBT) son tareas largas que congelan el hilo principal del navegador.

**Solución:** Implementé un **patrón Facade**. Creé un `ChatWrapper.astro` que renderiza un botón estático en HTML puro (0 JavaScript). Solo cuando el usuario hace clic, un `import()` dinámico carga React y el chat completo. El JavaScript se ejecuta en un momento de ocio del navegador, no durante el renderizado crítico.

```
<!-- Antes: React se hidrataba siempre, bloqueando el hilo principal -->
<ChatWidget client:load />

<!-- Después: HTML estático, React solo al hacer clic -->
<ChatWrapper />  <!-- botón HTML, 0 JS -->
```

El Speed Index bajó de **4.1s a 2.4s**. El TBT pasó de 80ms a **0ms** — la CPU del móvil ya no tiene que procesar React antes de mostrar la página.

![Lighthouse 97 móvil — tras eliminar GA y fonts](/images/97-movil.webp)

## Fix 4: Contraste WCAG — de 96 a 100 en accesibilidad

Lighthouse marcaba varios elementos con contraste insuficiente. La lección: **el mismo color puede fallar en un fondo y pasar en otro**. Hay que calcular contra el fondo real.

| Elemento | Antes | Después | Ratio | WCAG |
|----------|-------|---------|-------|------|
| Fechas blog (sobre blanco) | `text-gray-400` (2.95:1) | `text-gray-500` (5.74:1) | AA |
| Footer (sobre gray-900) | `text-gray-500` (3.67:1) | `text-gray-400` (~7:1) | AAA |
| Enlace cookies (sobre oscuro) | `text-primary` (2.2:1) | `text-indigo-400` (5.1:1) | AA |
| Badges servicios (sobre primary) | `bg-white/20` (4.19:1) | `border-white/40` (6.28:1) | AAA |

El caso de los badges es interesante: el fondo `bg-white/20` aclaraba el indigo, reduciendo contraste con el texto blanco. Cambié a un borde semitransparente — el texto se compara contra el fondo original, no contra el badge.

## Fix 5: Nginx no cacheaba .webp

Mi configuración de Nginx cacheaba png, jpg, svg... pero no `.webp`. El logo y todas las imágenes del blog se re-descargaban en cada visita.

**Solución:** Añadir `webp` y `avif` al regex de caché: 30 días, `immutable`.

## El resultado

![Lighthouse 100 móvil — resultado final](/images/100-movil.webp)

![Lighthouse 100 escritorio](/images/100-escritorio.webp)

| Métrica | Antes | Después | Mejora |
|---------|-------|---------|--------|
| Rendimiento (móvil) | 89 | **100** | +11 |
| Accesibilidad | 96 | **100** | +4 |
| SEO | 92 | **100** | +8 |
| FCP | 2.6s | **1.2s** | -54% |
| LCP | 3.2s | **1.4s** | -56% |
| Speed Index | 2.6s | **2.4s** | -8% |
| TBT | 80ms | **0ms** | -100% |

![Optimización Lighthouse — métricas de rendimiento web](/images/2026-05-asturwebs-central-lighthouse-rendimiento.webp)

## Lo que aprendí

**En un sitio estático, el rendimiento no se mejora con caché de servidor.** Se mejora eliminando lo que bloquea el hilo principal del navegador. JavaScript innecesario, fuentes externas, hidratación prematura.

Cada byte de JS que quitas del renderizado inicial es un paso hacia el 100.

Y si tienes un componente interactivo que el 90% de los usuarios no va a usar en los primeros segundos, **no lo cargues**. Ponle una fachada estática y cárgalo cuando te lo pida.

## Cómo evito que vuelva a bajar a 89

El rendimiento no es un destino, es un estado. Mi CI/CD en GitHub Actions builda y despliega automáticamente en cada push a `main`. Si un nuevo componente o un post pesado degrada las métricas, lo detecto en el siguiente deploy. La clave es que los patrones que he aplicado (self-hosting, facade, sin JS inicial) son estructurales — no dependen de un plugin que se actualiza y rompe todo.

---

<div class="not-prose my-12 bg-indigo-50 border border-indigo-100 rounded-2xl p-8 text-center">
<p class="text-xl font-semibold text-gray-900 mb-2">¿Tu web está en la zona roja o naranja?</p>
<p class="text-gray-600 mb-6">Cada segundo de carga te cuesta conversiones. Auditoría Lighthouse + plan de acción concreto.</p>
<a href="/contacto/" class="inline-block bg-primary text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-hover transition-colors">Audita mi web →</a>
</div>