5
Issues Críticos
ahorrable
en LCP con los fixes
82+
Score objetivo alcanzable
1.4MB
JS transferido (sin comprimir)
1. Core Web Vitals — Resumen
LCP
5.2s
Objetivo: < 2.5s · Umbral: 4s
INP
380ms
Objetivo: < 200ms · Umbral: 500ms
CLS
0.19
Objetivo: < 0.1 · Umbral: 0.25
TTFB
1.9s
Objetivo: < 800ms · Umbral: 1.8s
FCP
3.4s
Objetivo: < 1.8s · Umbral: 3s
TBT
780ms
Objetivo: < 200ms · Umbral: 600ms
Speed Index
6.1s
Objetivo: < 3.4s · Umbral: 5.8s
Lighthouse Desktop
61
Brecha mobile/desktop: 27 pts
2. Top Issues — Causas Raíz
1
Hero image no optimizada — 1.8MB PNG sin dimensiones
IMPACTO ALTO
LCP
CLS
El elemento LCP es
hero-dashboard.png (1.8MB, 3200×2000px). Se sirve sin atributos width/height, provoca layout shift al cargar (CLS +0.12), y no usa WebP ni AVIF. Sin fetchpriority="high" ni preload, el navegador la descubre tarde en el ciclo de render.
Ahorro estimado: −2.1s en LCP · −0.12 en CLS
2
Google Tag Manager + Hotjar síncronos en <head>
IMPACTO ALTO
FCP
TBT
GTM (
gtm.js 143KB) y Hotjar (hotjar.js 89KB) se cargan como recursos render-blocking desde <head>. Bloquean el primer pintado 620ms adicionales. Hotjar además activa un MutationObserver que contribuye +180ms al TBT.
Ahorro estimado: −620ms en FCP · −200ms en TBT
3
Bundle JS monolítico — sin code splitting dinámico
IMPACTO ALTO
TBT
INP
El chunk principal de Next.js incluye
chart.js (350KB), @fullcalendar/react (210KB) y lodash completo (71KB) — todos usados solo en páginas internas del dashboard, no en la landing. El main thread queda bloqueado parseando 631KB de JS innecesario.
Ahorro estimado: −380ms en TBT · −1.4MB transferidos
4
Fuentes Google Fonts sin font-display ni preconnect efectivo
IMPACTO MEDIO
FCP
CLS
Inter y Manrope se cargan con
font-display: auto (por defecto), causando FOIT de hasta 3s. Hay un <link rel="preconnect"> a fonts.googleapis.com pero falta el preconnect a fonts.gstatic.com (origen real de los .woff2). Cadena de red: Document → CSS de Google Fonts → .woff2 (3 saltos).
Ahorro estimado: −0.07 en CLS por font swap · −280ms en FCP
5
Sin caché eficiente en assets estáticos (Vercel default headers)
IMPACTO MEDIO
TTFB
JS y CSS chunks de Next.js tienen
Cache-Control: public, max-age=0, must-revalidate (Vercel default sin fingerprinting configurado). En revisitas, el navegador revalida cada asset con 304. Configurar max-age=31536000, immutable en chunks hasheados eliminaría estas peticiones.
Ahorro estimado: −310ms en carga repetida (visitas de retorno)
3. Análisis de Red — Recursos Críticos
| Recurso | Tipo | Tamaño | Carga | Cache-Control | Estado |
|---|---|---|---|---|---|
hero-dashboard.png |
Image | 1.8MB | 1320ms | no-cache | LCP bloqueante |
gtm.js (Google Tag Manager) |
Script | 143KB | 890ms | max-age=0 | RENDER BLOCKING |
hotjar.js |
Script | 89KB | 740ms | no-store | RENDER BLOCKING |
_next/static/chunks/main-app.js |
Script | 1.4MB | 2100ms | max-age=0, must-revalidate | SIN CODE SPLIT |
fonts.googleapis.com/css2?... |
Stylesheet | 4KB | 320ms | max-age=86400 | FOIT RISK |
Inter-Regular.woff2 |
Font | 92KB | 680ms (tarde) | max-age=31536000 | SIN PRELOAD |
screenshot-carousel-1.jpg |
Image | 340KB | 820ms | no-cache | DIFERIBLE |
_next/static/css/styles.css |
Stylesheet | 210KB | 410ms | max-age=0 | TAILWIND SIN PURGE |
4. Recomendaciones con Código
Fix #1 — Optimizar hero image con next/image + fetchpriority
Esfuerzo: bajo
Reemplaza el
<img> estático por el componente next/image con priority, añade dimensiones explícitas y convierte a WebP/AVIF (Next.js lo hace automáticamente). Esto elimina el CLS de la imagen y activa fetchpriority="high" en el LCP element.// Antes (app/page.tsx) <img src="/hero-dashboard.png" className="w-full" /> // Después import Image from 'next/image' <Image src="/hero-dashboard.png" alt="Dashboard de pedidos Velozito" width={1600} height={1000} priority // fetchpriority="high" + preload automático placeholder="blur" // Evita el FOUC mientras carga sizes="(max-width: 768px) 100vw, 50vw" /> // next.config.mjs — habilitar AVIF export default { images: { formats: ['image/avif', 'image/webp'], minimumCacheTTL: 31536000, } }
Fix #2 — Mover GTM y Hotjar a after-interactive
Esfuerzo: bajo
Usar el componente
next/script con strategy="afterInteractive" descarga los scripts de analytics fuera del camino crítico. El render ya habrá ocurrido antes de que se ejecuten.import Script from 'next/script' // app/layout.tsx <Script src=`https://www.googletagmanager.com/gtm.js?id=GTM-XXXX` strategy="afterInteractive" /> <Script id="hotjar" strategy="afterInteractive" > {`(function(h,o,t,j,a,r){ h.hj=h.hj||function(){...}; })(window,...)`} </Script> // Impacto: −620ms FCP, −180ms TBT
Fix #3 — Dynamic imports para librerías del dashboard
Esfuerzo: medio
Las librerías pesadas (Chart.js, FullCalendar, Lodash) solo se usan en rutas autenticadas. Importarlas dinámicamente evita que entren en el bundle de la landing.
// Antes (en app/page.tsx — se incluye en el bundle público) import { Chart } from 'chart.js' // 350KB import Calendar from '@fullcalendar/react' // 210KB import _ from 'lodash' // 71KB // Después (en app/dashboard/[...]/page.tsx) const Chart = dynamic(() => import('chart.js').then(m => m.Chart), { ssr: false, loading: () => <ChartSkeleton /> }) const Calendar = dynamic(() => import('@fullcalendar/react'), { ssr: false }) // Para lodash: importar solo las funciones usadas import groupBy from 'lodash/groupBy' // 5KB vs 71KB
Fix #4 — Auto-alojar fuentes con font-display: swap
Esfuerzo: medio
Next.js 13+ incluye
next/font que auto-aloja Google Fonts en Vercel Edge, eliminando la cadena de red de 3 saltos y aplicando font-display: swap automáticamente.// app/layout.tsx import { Inter, Manrope } from 'next/font/google' const inter = Inter({ subsets: ['latin'], display: 'swap', // Evita FOIT preload: true, variable: '--font-inter', }) const manrope = Manrope({ subsets: ['latin'], display: 'swap', variable: '--font-manrope', weight: ['400', '600', '700'], // Solo los weights usados }) // Eliminar estos tags del <head>: // <link href="https://fonts.googleapis.com/css2?..." /> // <link rel="preconnect" href="https://fonts.gstatic.com" />
Fix #5 — PurgeCSS en producción + caché inmutable en chunks
Esfuerzo: bajo
Tailwind en producción requiere configurar el campo
content para tree-shake clases no usadas. Además, los chunks de Next.js con hash en el nombre deben ser inmutables. Añadir vercel.json con headers de caché.// tailwind.config.ts — Fix PurgeCSS export default { content: [ './app/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx}', ], // Sin esto, Tailwind incluye ~4MB de CSS en dev-mode } // vercel.json — Cache-Control inmutable para chunks hasheados { "headers": [{ "source": "/_next/static/(.*)", "headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }] }] }
5. Accesibilidad — Gaps Detectados
Contraste insuficiente en CTAs
Botón primario (#6366f1 sobre #fff): ratio 3.2:1 < 4.5:1 WCAG AA para texto normal.
✖ FALLO WCAG AA
Sin indicador de foco visible
CSS global:
*:focus { outline: none } elimina el anillo de foco en todos los elementos interactivos.✖ FALLO WCAG 2.4.7
Imágenes sin alt text
4 imágenes en el carrusel de screenshots tienen
alt="" vacío o ausente.⚠ MEJORA RECOMENDADA
Target táctil < 44×44px
Íconos de redes sociales en footer: 24×24px. iOS/Android requieren mínimo 44px por WCAG 2.5.5.
⚠ MEJORA RECOMENDADA
6. Matriz de Priorización — Impacto vs Esfuerzo
Acción
Impacto LCP
Esfuerzo
Score Delta
Fix #1 — Hero image con next/image
−2.1s
Bajo (2h)
+22 pts
Fix #2 — Scripts de analytics afterInteractive
−620ms FCP
Bajo (1h)
+14 pts
Fix #3 — Dynamic imports dashboard libs
−380ms TBT
Medio (4h)
+10 pts
Fix #4 — Auto-hosting fuentes next/font
−280ms FCP
Bajo (1h)
+6 pts
Fix #5 — PurgeCSS + caché Vercel
Visitas repetidas
Bajo (2h)
+6 pts
🎯
Resultado estimado tras implementar los 5 fixes
Lighthouse Mobile: 34 → 82+ · LCP: 5.2s → ~2.0s · TBT: 780ms → ~200ms · CLS: 0.19 → 0.04
Esfuerzo total estimado: ~10 horas de desarrollo · Plazo recomendado: sprint de 2 días