
Vecility.com marca 99 en Lighthouse mobile. No en una conexión rápida — en un Moto G Power emulado con 4G lenta. FCP: 1.0s. LCP: 1.4s. TBT: 40ms. CLS: 0. Accesibilidad: 100. SEO: 100.
No es una página HTML estática. Es una landing SaaS bilingüe con animaciones de partículas en canvas, navegación interactiva, FAQ con accordion, y múltiples islas de React — corriendo en un VPS de Hetzner, no en Vercel ni ninguna plataforma edge.
Vecility es nuestro propio producto — una plataforma SaaS white-label para agencias de courier, construida por Deploytive. Compartimos este caso de estudio abiertamente porque pueden verificar cada número: abran Chrome DevTools en vecility.com y corran Lighthouse ahora mismo.
Estos son los problemas que encontramos, qué los causaba, y cómo los resolvimos.
El punto de partida: 90 en desktop, 63 en mobile
Cuando corrimos Lighthouse después de lanzar Vecility, desktop marcó 98. Casi seguimos adelante.
Luego corrimos el test en mobile con throttling de 4G lenta — las condiciones reales de nuestro mercado objetivo. El score fue 63. FCP: 4.6 segundos. LCP: 6.7 segundos. Nuestros usuarios estaban viendo una pantalla en blanco durante casi 5 segundos antes de leer una sola palabra.
Desktop miente. Un sitio que se siente instantáneo en su MacBook con fibra puede ser inutilizable en un Android de gama media con conexión celular. Desde ese momento, nos comprometimos a probar exclusivamente en mobile con throttling habilitado.
Problema 1: El texto del hero tardaba 2.7 segundos en aparecer
Qué vimos: Lighthouse señaló el elemento LCP — el headline del hero — con un “retraso de renderización” de 1,460ms. El servidor respondía en 0ms (Time to First Byte perfecto). El HTML ya estaba en el browser. Pero no aparecía ningún texto.
El diagnóstico: El browser estaba esperando a que un archivo de tipografía (Plus Jakarta Sans, peso 900) se descargara desde los servidores de Google Fonts antes de renderizar una sola letra. Esto requería un DNS lookup a fonts.googleapis.com, una negociación TLS, descarga de un archivo CSS, luego un segundo DNS lookup a fonts.gstatic.com, una segunda negociación TLS, y finalmente la descarga del archivo de font. En desktop con fibra óptica, esto toma unos 100ms. En un Moto G Power con 4G lenta, tomaba 1,460ms.
Cómo lo resolvimos: Descargamos los archivos de tipografía y los servimos desde nuestro propio servidor — eliminando ambas conexiones externas completamente. Configuramos las fonts con font-display: swap, que le indica al browser: “pinta el texto inmediatamente con la tipografía del sistema, y cuando llegue la font custom, haz el intercambio.”
Un detalle crítico que descubrimos: font-display: swap solo funciona si se define un fallback stack de fonts. Si se le dice al browser “usa swap” pero no se le da una alternativa, no tiene a qué hacer swap. Agregamos las fonts por defecto del sistema operativo como cadena de respaldo.
También solo precargamos las 2 variantes de font que aparecen above-the-fold (el peso del headline y el del body text). Las fonts que se usan más abajo en la página — como la monoespaciada para elementos de código — se dejan cargar naturalmente cuando el browser las necesita.
El resultado: El headline del hero pasó de pintar a los 1,460ms a pintar en aproximadamente 50ms. Solo por servir las fonts desde el mismo servidor en vez de depender de Google.
Problema 2: Google Analytics bloqueaba el primer render
Qué vimos: El script de Google Analytics (gtag.js) estaba en el <head> con async. En desktop eso no importa — sobra ancho de banda. En 4G lenta, 150KB de JavaScript compiten con las fonts y el CSS por el mismo ancho de banda limitado.
El diagnóstico: async no significa “no bloquea”. Significa “descarga en paralelo pero ejecuta tan pronto como llegue”. En una conexión lenta, gtag.js llegaba ANTES que las fonts del hero, y el browser le daba prioridad de ejecución. El usuario no necesita analytics para ver la página — pero el browser no lo sabe.
Cómo lo resolvimos: Movimos Google Analytics a carga diferida: el script se inyecta 2 segundos después de que la página ya pintó. El tracking sigue funcionando exactamente igual — solo empieza a contar 2 segundos después del page load. Ningún usuario nota la diferencia. Pero el browser tiene 150KB menos compitiendo por el ancho de banda durante el momento crítico del primer render.
El resultado: 150-300ms menos en FCP en conexiones lentas. Analytics sigue reportando las mismas métricas en GA4 sin pérdida de datos.
Problema 3: La barra de navegación forzaba la carga de React completo
Qué vimos: En el árbol de dependencias de red, la barra de navegación (Navbar) aparecía como el nodo más pesado: 1,306ms. Detrás de ella venía todo el runtime de React — jsx-runtime, el index del framework, y el código de hidratación. Total: aproximadamente 182KB de JavaScript que debían descargarse, parsearse y ejecutarse antes de que el hero debajo pudiera pintar.
El diagnóstico: Astro permite usar componentes React con una directiva llamada client:load que le dice al framework: “hidrata este componente inmediatamente al cargar la página.” El navbar usaba esa directiva. El problema es que un navbar es HTML estático con dos interacciones simples: comportamiento sticky al hacer scroll y un menú hamburguesa en mobile. Eso no necesita React.
Pero como el navbar usaba React con client:load, forzaba la descarga de todo el runtime de React ANTES de que el browser pudiera pintar cualquier cosa debajo — incluyendo el hero con el headline y el subheadline (nuestro elemento LCP).
Cómo lo resolvimos: Reescribimos el navbar como un componente Astro puro — HTML estático con un script inline de JavaScript vanilla. El sticky scroll son 3 líneas de JS. El hamburguesa son 5 líneas más. El toggle de idioma es simplemente un link que navega entre / y /en/. Ninguna de estas funcionalidades necesita un framework de 182KB.
Al eliminar React del navbar, React solo se carga cuando el usuario hace scroll hasta componentes que realmente lo necesitan (el canvas de partículas, el formulario de contacto) — gracias a la directiva client:visible que hidrata componentes solo cuando entran al viewport.
El resultado: 1,306ms eliminados de la cadena crítica. El hero pinta inmediatamente sin esperar a que React se descargue, se parsee y se ejecute.
Lección aprendida: Revisamos todo el proyecto buscando client:load y lo eliminamos completamente. Regla: si un componente no necesita estado de React, interactividad compleja, o re-renders dinámicos, no debería ser React. HTML + JS vanilla es suficiente y es gratis en términos de performance.
Problema 4: El CSS de una sección invisible bloqueaba todo
Qué vimos: En el árbol de dependencias, apareció un archivo CSS de 42KB que formaba parte de la cadena crítica. Era el CSS del componente FAQ — una sección al fondo de la página, invisible sin hacer scroll.
El diagnóstico: Astro extrae los estilos de cada componente a archivos CSS separados y los incluye como <link> en el <head>. Esto es bueno para caching, pero es un problema cuando un componente below-the-fold tiene estilos pesados — porque el browser no pintará NADA hasta que TODOS los CSS del <head> se descarguen.
Nuestro FAQ tenía estilos para el accordion (animaciones de apertura/cierre, transiciones, bordes) que sumaban 42KB. Esos 42KB bloqueaban el primer paint del hero que está 3,000 pixels más arriba.
Cómo lo resolvimos: Configuramos los estilos del FAQ para que se rendericen inline donde el componente aparece en el HTML, no como un archivo externo en el <head>. Cuando el browser llega al <head>, no encuentra esos 42KB bloqueantes. Los estilos del FAQ se cargan naturalmente cuando el browser hace scroll hasta esa sección.
El resultado: 543ms eliminados de la cadena crítica. El FAQ sigue viéndose y funcionando exactamente igual — pero sus estilos ya no penalizan el primer render.
Problema 5: La animación de partículas forzaba recálculos de layout
Qué vimos: Lighthouse reportaba un warning de “reprocesamiento forzado” — el browser estaba siendo forzado a recalcular el layout de toda la página en momentos inapropiados.
El diagnóstico: La animación de partículas del canvas necesitaba conocer las dimensiones de su contenedor para ajustar el tamaño del canvas. Al inicializarse y al redimensionar la ventana, el código leía las dimensiones del contenedor inmediatamente después de modificar el canvas. Esto fuerza al browser a detener todo, recalcular el layout completo de la página, y devolver los valores. En un celular de gama media, eso bloquea el main thread por 10-50ms cada vez.
Cómo lo resolvimos: Sincronizamos la lectura de dimensiones con el ciclo de renderizado del browser. En vez de leer las dimensiones inmediatamente (forzando el recálculo), le indicamos al browser: “la próxima vez que vayas a pintar, dame las dimensiones.” El browser las calcula una sola vez como parte de su ciclo normal, sin interrupciones.
También agregamos un delay de 1 segundo en mobile antes de iniciar la animación de partículas. Esto permite que el hero pinte primero y el usuario vea contenido inmediatamente — las partículas aparecen un segundo después. En desktop no hay delay.
El resultado: El warning de reprocesamiento forzado desapareció. El TBT (Total Blocking Time) se mantuvo en 40ms — excelente para una página con animaciones de canvas.
Los números finales
| Métrica | Antes | Después |
|---|---|---|
| Performance (mobile) | 63 | 99 |
| FCP | 4.6s | 1.0s |
| LCP | 6.7s | 1.4s |
| TBT | 50ms | 40ms |
| CLS | 0.007 | 0 |
| Speed Index | 5.9s | 1.4s |
| Accesibilidad | 100 | 100 |
| SEO | 100 | 100 |
Estas métricas son de un Moto G Power emulado con 4G lenta — no de un MacBook Pro con fibra óptica. Son las condiciones reales de nuestro mercado objetivo.
No confíe solo en nuestra palabra — corra el test usted mismo: PageSpeed Insights para vecility.com.

Lo que aprendimos
Desktop miente. Un sitio puede marcar 98 en desktop y 63 en mobile. Si no prueba en mobile con throttling de 4G lenta, no sabe cómo experimentan su sitio los usuarios reales.
“Async” no significa “no bloquea.” Scripts, fonts y CSS con async siguen compitiendo por ancho de banda. En conexiones lentas, todo lo que no sea esencial para el primer render debe cargarse después.
Los frameworks son caros. 182KB de React para un menú hamburguesa es un lujo que los usuarios en 4G no pueden pagar. La pregunta correcta no es “¿puedo usar React aquí?” sino “¿necesito React aquí?”
Los problemas se encadenan. Cada recurso bloqueante no solo suma su propio tiempo — crea una cadena. Font que depende de CSS que depende de DNS. La solución no es optimizar cada eslabón sino romper la cadena.
90 no es suficiente. 90 en Lighthouse suena bien hasta que ve que su usuario espera 4.6 segundos para leer el primer texto. Performance no es un score — es la experiencia real de una persona esperando a que su pantalla deje de estar en blanco.
Sobre este proyecto
Vecility es una plataforma SaaS white-label para agencias de courier, construida por Deploytive. Aplicamos el mismo rigor de ingeniería de performance a cada sistema que construimos — plataformas SaaS, apps móviles, herramientas enterprise.
Si su sitio o aplicación es lenta y no está seguro por qué, podemos ayudar a diagnosticar el problema y resolverlo.