Skip to content
11 min de lectura

El Camino al 100: Cómo Logré Puntajes Perfectos en Lighthouse en Todas las Categorías

El Camino al 100: Cómo Logré Puntajes Perfectos en Lighthouse en Todas las Categorías

Escribí sobre cómo construí XergioAleX.com — la arquitectura, el stack tecnológico, el viaje de una landing de una sola página a una plataforma personal completa. Al final de ese post mencioné que uno de mis objetivos era alcanzar puntajes perfectos en Google Lighthouse: 100 en Performance, Accesibilidad, Mejores Prácticas y SEO.

Lo prometido es deuda. Este post es la historia de cómo lo logré.

La verdad es que Astro te da un inicio increíble. Por defecto, la mayoría de proyectos Astro salen con puntajes en los 90s en casi todas las categorías. El framework fue diseñado pensando en performance — genera HTML estático, envía cero JavaScript innecesario, y trae image optimization de cajón. Llegar a 90 es casi automático. Pero los últimos puntos para llegar al 100 — ahí es donde vive el verdadero trabajo. Esa brecha entre “excelente” y “perfecto” requiere atención a los detalles que a veces se sienten obsesivos, pero que en conjunto marcan una diferencia real.

Permíteme contarte exactamente qué cambié y por qué.


Accesibilidad — La Base (100/100)

Este fue el trabajo más extenso de todo el proceso. Una auditoría sistemática que tocó componentes del blog, secciones del home, el layout principal y páginas individuales — un proceso que me hizo pensar diferente sobre cómo construyo interfaces.

Contraste de color: el problema invisible

El primer hallazgo que me sorprendió fue el contraste. Lighthouse señaló que varios textos no cumplían el ratio mínimo de 4.5:1 que exige el estándar WCAG AA.

El culpable en casi todos los casos era el mismo: text-gray-400, text-gray-500, dark:text-gray-400 y dark:text-gray-500 — colores grises claros que se veían bien visualmente pero que en realidad no tienen suficiente contraste para ser legibles por personas con baja visión o en condiciones de luz difícil. Tocó revisar componentes del blog, secciones del home, el layout principal, páginas individuales y el timeline.

La solución fue sistemática: reemplazar esos valores por text-gray-600 dark:text-gray-300. El gray-600 (#4b5563) y el gray-300 (#d1d5db) mantienen la estética gris y sutil que buscaba — no hay un cambio visual dramático — pero pasan WCAG AA sin problema. Es ese tipo de ajuste que una vez que lo ves documentado, no puedes dejar de notar en otros sitios.

ARIA semántico: el patrón del menú

Los dropdowns del header del sitio usaban role="menu" — y resulta que eso está técnicamente mal para un caso de uso de navegación. El patrón role="menu" está diseñado para menús de aplicación (think: un menú Archivo/Editar/Ver en un editor de texto), no para listas de links de navegación.

El patrón correcto para un menú de navegación con dropdown es disclosure — usando aria-expanded y aria-controls en el botón que abre el panel, sin role="menu". Es un cambio que no afecta nada visualmente, pero le dice correctamente a los screen readers qué están manejando.

Otros ajustes ARIA que hice en paralelo:

  • Las barras de progreso de skills recibieron role="progressbar" con atributos aria-valuenow, aria-valuemin y aria-valuemax
  • Imágenes puramente decorativas recibieron aria-hidden="true" para que los lectores de pantalla las ignoren

Skip-to-content y el landmark <main>

Agregué un skip-to-content link al MainLayout.astro — ese enlace invisible que aparece solo cuando un usuario de teclado presiona Tab por primera vez, permitiéndole saltar directamente al contenido principal sin tener que navegar por toda la barra de navegación. Es una de esas funcionalidades que la mayoría de los usuarios nunca verá, pero para quienes dependen del teclado o un lector de pantalla, es la diferencia entre una experiencia usable y una frustrante.

Dimensiones explícitas en imágenes

Cada <img> del sitio recibió atributos explícitos width y height. Esto es técnicamente un punto de performance también — previene el Cumulative Layout Shift (CLS) porque el navegador puede reservar el espacio antes de que la imagen cargue — pero Lighthouse también lo audita bajo accesibilidad porque tiene implicaciones para el layout en general.

Esto significó revisar imagen por imagen en todos los componentes. Tedioso pero necesario.

Alt text de imágenes

Una auditoría cuidadosa reveló varias imágenes con alt text faltante o demasiado genérico (“image”, “photo”, cosas así). Cada una recibió texto descriptivo apropiado, o alt="" en los casos donde la imagen es puramente decorativa.


Performance — El 100 Más Difícil

Si la accesibilidad fue la más laboriosa, performance fue la más creativa. Aquí el reto no es solo seguir un checklist — es entender qué está bloqueando el render y encontrar soluciones que no comprometan la experiencia.

La historia del Typewriter: el cambio más interesante

Developer Builder Creator Maker Explorer

Esto que ves es 100% CSS — cero JavaScript. La misma animación que está en el hero del sitio.

En el hero section del sitio había un componente Typewriter construido en Svelte (client:idle) que animaba palabras rotativas — “Developer”, “Builder”, “Creator”, ese tipo de cosa. Se veía bien. Funcionaba bien. Y Lighthouse lo identificó como parte de la cadena de peticiones críticas, añadiendo 1,338ms de latencia al tiempo de carga inicial.

El componente Svelte, por más client:idle que fuera, seguía siendo JavaScript que el navegador tenía que descargar, parsear y ejecutar. Y ese JavaScript estaba en el critical path del hero section.

La solución que encontré fue la más satisfactoria técnicamente de todo el proyecto: reemplazar el componente Svelte completamente con CSS puro.

El nuevo TypewriterWords.astro logra exactamente el mismo efecto — tipeo carácter por carácter, borrado, transición a la siguiente palabra — usando únicamente CSS:

  • Cada palabra usa max-width expresado en unidades ch (character widths), animado con animation-timing-function: steps(N, end) donde N es el número de caracteres de esa palabra
  • Astro genera @keyframes únicos por palabra en tiempo de build, así cada animación tiene exactamente los pasos correctos
  • El cursor es simplemente un border-right que sigue naturalmente el borde del max-width — no hay un elemento separado para el cursor
  • El centrado funciona con una arquitectura de tres capas: .tw-word para la posición, .tw-sizer para el ancho animado, .tw-text para el texto visible
  • El componente respeta prefers-reduced-motion — si el usuario tiene activada esa preferencia, las palabras simplemente se muestran sin animación

El resultado: el chunk de JavaScript del Typewriter desapareció del bundle completamente. La latencia de la ruta crítica bajó de 1,338ms a 468ms — una reducción del 65%.

Ese es el tipo de optimización que se siente bien no solo porque mejora los números, sino porque el código resultante es más simple, más robusto y más accesible.

Imágenes responsive con srcset

Implementé variantes WebP en múltiples tamaños para que los dispositivos móviles descarguen exactamente el tamaño que necesitan y nada más. El ahorro en mobile es significativo cuando estás optimizando cada kilobyte.

Optimización SVG con SVGO

Los SVG del sitio no estaban optimizados. Pasar todo por SVGO redujo el tamaño del logo del hero y del header de forma notable — menos bytes para descargar, misma calidad visual.

Además, agregué un <link rel="preload"> para el logo del header, que es el elemento visual más prominente en el above-the-fold y vale la pena prioritizar.

font-display: swap y CSS inline

Para la fuente personalizada Atkinson Hyperlegible, configuré font-display: swap — así el texto se muestra inmediatamente usando la fuente de sistema mientras se carga la fuente personalizada, en lugar de quedar invisible durante la carga.

También configuré build.inlineStylesheets: 'always' en Astro, que inline el CSS directamente en el HTML. En un sitio estático como este, el CSS es pequeño y la eliminación de la petición de red adicional tiene más impacto que cualquier penalización por tamaño del HTML.


SEO — La Capa de Infraestructura (100/100)

La auditoría de SEO reveló varias cosas que no sabía que faltaban.

Datos estructurados: de 3 esquemas a 7

El sitio ya tenía algunos schemas JSON-LD, pero eran básicos. Expandí la cobertura de 3 tipos de Schema.org a 7:

  • WebSite — Información base del sitio
  • Person — Mi perfil como autor/propietario
  • Organization — La presencia profesional
  • BlogPosting — En cada post del blog
  • BreadcrumbList — Navegación estructurada para buscadores
  • CollectionPage — En la página de listado del blog
  • ContactPage — En la página de contacto

Los datos estructurados no mueven el puntaje de Lighthouse directamente, pero son parte de la auditoría SEO y tienen impacto real en cómo Google entiende y presenta el contenido en los resultados de búsqueda.

Página 404 personalizada

El sitio no tenía página 404. Creé NotFoundPage.astro — bilingüe, con dark mode, siguiendo el mismo diseño del resto del sitio. Pequeño detalle que hace que la experiencia se sienta completa.

RSS por idioma: un bug que no sabía que tenía

Al auditar el sitio español, descubrí que todas las páginas en español apuntaban al RSS feed en inglés. El <link rel="alternate" type="application/rss+xml"> en el <head> no estaba siendo localizado. Fix sencillo pero importante — si alguien se suscribe al RSS desde la versión española del sitio, debería recibir los posts en español.

Optimización para crawlers de IA

Una auditoría del llms.txt y llms-full.txt reveló que el contenido estaba triplicado y duplicado respectivamente — resultando en archivos inflados con información repetida. Los reescribí completamente. También agregué crawlers de IA adicionales a robots.txt que no estaban listados.

Web app manifest

Generé un site.webmanifest completo con iconos PWA en múltiples tamaños, producidos programáticamente desde el favicon SVG. No convierte el sitio en una PWA funcional, pero completa la infraestructura que los navegadores modernos esperan y que Lighthouse audita.


Mejores Prácticas — Los Detalles (100/100)

Esta categoría fue la más rápida de resolver porque los problemas eran más aislados.

Auditoría de hidratación

Revisé los componentes Svelte del sitio que estaban usando client:load — la directiva de hidratación más agresiva, que hidrata el componente inmediatamente al cargar la página. Para componentes que no necesitan ser interactivos de inmediato (un grid de posts, la sección de búsqueda, algunos elementos del home), cambié a client:visible, que hidrata solo cuando el componente entra al viewport.

La regla que aplico ahora: client:visible o client:idle por defecto, client:load solo cuando la interactividad inmediata es crítica (como el header de navegación).

Lazy loading en imágenes de BlogCard

Las tarjetas del blog ahora usan loading="lazy" en sus imágenes. Las imágenes fuera del viewport inicial no se descargan hasta que el usuario hace scroll hacia ellas. Para una página de listado con decenas de posts, el ahorro en la carga inicial es considerable.

Headers de cache en Cloudflare

Configuré Cloudflare para servir assets hasheados (JS, CSS, imágenes) con Cache-Control: public, max-age=31536000, immutable — cache de un año completo, marcado como inmutable. Los assets hasheados nunca cambian en la misma URL, así que el browser puede cachearlos para siempre sin preocuparse por recibir versiones obsoletas.

Favicon fallback

Agregué un favicon.ico tradicional para navegadores que no soportan SVG. Detalle pequeño, pero Lighthouse lo nota.


Los Resultados

PageSpeed Insights — Móvil: 100 en Performance, Accesibilidad, Mejores Prácticas y SEO. Core Web Vitals: FCP 0.9s, LCP 1.5s, TBT 0ms, CLS 0, Speed Index 0.9s

PageSpeed Insights — Escritorio: 100 en Performance, Accesibilidad, Mejores Prácticas y SEO. Core Web Vitals: FCP 0.3s, LCP 0.3s, TBT 0ms, CLS 0, Speed Index 0.5s

MétricaMóvilEscritorio
First Contentful Paint0.9s0.3s
Largest Contentful Paint1.5s0.3s
Total Blocking Time0 ms0 ms
Cumulative Layout Shift00
Speed Index0.9s0.5s

El TBT de 0ms en ambas plataformas es el que más me satisface. Significa que el hilo principal del navegador nunca se bloqueó por más de 50ms durante la carga — la experiencia se siente instantánea porque lo es.


Reflexiones finales

Si tuviera que elegir lo más impactante de todo este proceso, elegiría el trabajo de accesibilidad. No porque sea lo más visible — la mayoría de usuarios nunca notará la diferencia — sino porque es lo más correcto. Asegurarme de que el sitio sea usable por personas con baja visión, que dependen del teclado, o que usan lectores de pantalla no es un ejercicio de optimización de métricas. Es construir algo que funciona para todos, no solo para los usuarios que se parecen a mí.

El cambio técnicamente más satisfactorio fue el Typewriter CSS-only. Hay algo genuinamente elegante en resolver un problema de performance eliminando JavaScript por completo en lugar de optimizarlo. El componente Svelte tenía más código, era más frágil, y requería el motor de JavaScript para hacer su trabajo. El reemplazo en CSS puro hace lo mismo con zero bytes de JavaScript, zero dependencias, y soporte nativo para prefers-reduced-motion. Cuando puedes resolver un problema con menos herramientas de las que empezaste, generalmente estás yendo en la dirección correcta.

Lo que también aprendí es que el performance es un compromiso continuo, no un estado final. Los puntajes son perfectos hoy porque hice este trabajo hoy. Cada nueva funcionalidad, cada imagen nueva, cada componente Svelte que agregue en el futuro es una oportunidad de introducir regresiones. Los puntajes de Lighthouse son una forma de mantener esa presión — una señal de que el estándar se puede mantener si se construye con la mentalidad correcta desde el principio.

Si estás construyendo un sitio Astro y quieres llegar al 100, mi recomendación es empezar por la accesibilidad. No porque sea lo más difícil — aunque lo es — sino porque es el trabajo que más importa. El performance y el SEO son optimizaciones. La accesibilidad es una responsabilidad.

A seguir construyendo.


Recursos