Cómo hice mi portafolio con Next.js 16 y GSAP (y saqué 99 en Lighthouse móvil)

Quería un portafolio que no fuera una lista de proyectos, sino algo que se sintiera vivo al hacer scroll y que contara cómo llegué hasta aquí. Lo construí desde cero con Next.js 16, TypeScript y GSAP, y en el camino tuve que aprender a que tantas animaciones no lo pusieran lento. Este es el resumen de cómo lo hice.
Un sitio estático, sin servidor
El portafolio usa el App Router de Next.js con exportación estática: al compilar se generan las páginas en HTML y no hace falta un servidor de Node. Las sirve Nginx dentro de un contenedor de Docker, desplegado en Dokploy.
- Cada proyecto tiene su propia página, generada con
generateStaticParams: una dirección por proyecto es mejor para Google que todo en una sola página. - El contenido (textos, proyectos, mi historia) vive como datos en TypeScript, separado de los componentes: puedo cambiar un texto sin tocar el diseño.
- Cada sección de la portada es una carpeta con su componente, sus estilos (CSS Modules) y su animación.
Animaciones con scroll que no frenan la página
Las animaciones están hechas con GSAP y ScrollTrigger: la vitrina oscura que sube sobre la portada, el bento y el carrusel horizontal de proyectos. El problema es que tanto JavaScript al cargar la página la vuelve lenta en el celular. Esto fue lo que funcionó:
- Cargar GSAP solo cuando hace falta, con
import()dinámico, en vez de meterlo en el paquete inicial. - Preparar las animaciones de más abajo cuando el navegador está libre (o con el primer scroll), no todas al cargar.
- Para el carrusel horizontal usé
position: stickyde CSS con la altura reservada en el CSS, en vez de fijar la sección con GSAP: fijarla después de cargar movía los enlaces a las secciones (/#contacto). - La animación de las tarjetas de la portada usa variables de CSS y no
transformen los fotogramas: contransform, Chrome contaba tarde la imagen principal y el LCP subía a unos 6 segundos.
Con eso la portada llegó a 99 en Lighthouse en móvil, con un tiempo de bloqueo total de unos 90 ms. Y quien tiene activado "reducir movimiento" en su sistema ve todo de una vez, sin animaciones.
Imágenes livianas sin servidor de imágenes
Con la exportación estática, Next.js no puede optimizar las imágenes al vuelo. Escribí un script con sharp que, antes de compilar, genera cada imagen en WebP y en varios tamaños, y un loader propio para que next/image elija el tamaño justo según la pantalla.
Pruebas y despliegue
- Más de 40 pruebas de punta a punta con Playwright sobre el sitio compilado, en escritorio y en celular: que las animaciones funcionen, que el menú lleve a cada sección y que cada página tenga su título, su descripción y sus datos para Google.
- GitHub Actions corre las pruebas en cada cambio.
- El build de Docker falla si falta el dominio del sitio: así nunca se publica con direcciones equivocadas en el sitemap.
SEO desde el principio
Cada página tiene su título, su descripción, su dirección canónica y su imagen para redes. El sitio genera su sitemap.xml y su robots.txt, y le da a Google datos estructurados: quién soy (Person), el sitio, mi historia (ProfilePage), las migas de pan de los proyectos, los videos y los artículos de este blog. Después de enviarlo a Google Search Console, al día siguiente ya salía primero al buscar mi nombre.
Lo que me llevo
Animar es la parte fácil; lo difícil es que se sienta fluido en un celular normal. Medir con Lighthouse antes y después de cada cambio, preferir CSS cuando alcanza y tener pruebas que me avisen si rompo algo me dejó cambiar mucho sin miedo.