Caso de estudio · Mi propio portfolio · 2026

Cada animación es una decisión.

Un portfolio visual puede ser un escaparate de efectos o la prueba de que sé decidir. Este caso recorre cada pieza de movimiento, tema, sonido e interacción de rodrigonegrete.es: qué problema resuelve, qué investigué, qué descarté y cuánto cuesta. Con demos que puedes probar y con lo que salió mal.

Rol
Dirección de diseño, criterio y revisión; implementación asistida por agentes de IA
Stack
Astro 7, islas React 19, CSS y JS vanilla
Periodo del rediseño
22 y 23 de octubre de 2026 (45 commits)
Tipo
Proyecto propio, sin cliente ni datos de uso
Tarjeta de perfil de la portada del portfolio en el tema Dual Core AI: retrato, AI Product Designer, 2+ años de UX/IA, 11 casos, 50+ herramientas y selector de temas
Tarjeta de perfil de la portada, tema por defecto. Captura real.

01 · Resumen y rol

Un portfolio que enseña su proceso, no solo su resultado.

Busco un rol de AI Product o UX/AI. Mi perfil es visual, y un perfil visual tiene un riesgo: que el movimiento parezca decoración. Por eso traté mi propio sitio como un producto con requisitos, medición y revisión.

Qué es

El sitio en rodrigonegrete.es: portada, proyectos, laboratorio, trayectoria, cinco temas visuales, tres idiomas (es, ca, en) y una página de caso por proyecto.

Mi parte

Las decisiones de diseño, los criterios de movimiento, la selección de referentes, la revisión de cada entrega y las mediciones. La implementación la hicieron agentes de IA especializados (Claude Code) que yo dirigía; 37 de los 45 commits del rediseño llevan el trailer Co-Authored-By.

Lo que no es

No hay cliente, ni analítica de uso, ni tests con usuarios. Cualquier frase sobre qué «funciona mejor» para quien visita el sitio es una hipótesis y está marcada como tal.

Verificado se puede comprobar en git log, en el código o en una medición indicada Lectura mía interpretación de diseño Hipótesis por validar

Lo que no lleva etiqueta dentro de una ficha es verificable en el repositorio. Los números de este caso salen de tres sitios: el historial de git, el código fuente y las mediciones de Lighthouse móvil en local que indico en cada gráfico.

  1. 02 Problema y objetivos
  2. 03 Proceso de investigación
  3. 04 Principios de movimiento
  4. 05 Sistema de movimiento (9 piezas, 6 demos)
  5. 06 Sistema de 5 temas
  6. 07 Accesibilidad y reduced-motion
  7. 08 Rendimiento
  8. 09 Arquitectura: de Vite a Astro con islas React
  9. 10 Agentes y QA
  10. 11 Errores y aprendizajes
  11. 12 Qué mediría y qué falta validar

02 · Problema y objetivos

Impresionar sin penalizar a quien visita.

Quien revisa un portfolio suele tener poco tiempo, a veces desde el móvil y a veces con el sonido apagado. Un efecto vistoso que retrasa el primer texto, tapa contenido o ignora las preferencias del sistema juega en contra del mensaje. Y a la vez, un portfolio de diseño sin criterio de interacción no demuestra lo que dice saber hacer.

Lectura mía

El problema de usuario

Dos lectores opuestos: quien tiene prisa y necesita ver mi perfil y mis casos rápido, y quien quiere ver criterio de diseño y se detiene en los detalles. El movimiento debe servir al segundo sin estorbar al primero.

Objetivos medibles que fijé

  • Lighthouse móvil: rendimiento ≥ 90; accesibilidad y SEO en 100.
  • Todo el movimiento bajo prefers-reduced-motion.
  • Nada de secuestro de scroll ni scroll-snap vertical obligatorio.
  • El contenido es visible por defecto: sin JS no se oculta nada.
  • Cada pieza con su porqué y su coste anotados.
Honestidad de partida. Estos objetivos son míos y no hay línea base de usuarios. Los comprobables (Lighthouse, reduced-motion, ausencia de scroll-jacking) están medidos o revisados; los de percepción no.

03 · Proceso de investigación

Estudiar a los mejores, y copiar cero líneas.

Antes de escribir CSS estudié sitios y librerías de referencia. Me interesaba entender qué técnica sostiene cada efecto y qué cuesta, no reproducirlos.

Referentes estudiados. Licencias según la investigación del proyecto; no las he vuelto a consultar al redactar este caso.
ReferenteLicenciaQué aporta Lectura míaQué hice
darkroomengineering/lenisMITScroll suave por JavaScriptDescartado: choca con mi regla de no tocar el scroll. Sin dependencia.
locomotivemtl/locomotive-scrollMITAnimaciones ligadas al scroll con motor propioEstudiado; lo equivalente lo hace animation-timeline nativo.
Cuberto/mouse-followerMITCursor propio con estados por elementoIdea de estados (botón, tarjeta) reescrita en código propio.
activetheory/split-textMITSeparar el texto en letras para animarloIdea reescrita a mano para el titular de la portada.
darkroomengineering/satusMITEstructura de proyecto y buenas prácticasSolo lectura de referencia.
basementstudio/basement-laboratorySin licenciaInspiración visual y de ritmoSolo inspiración. No se copió código.
makemepulse/2024-kaizen-publicSin licenciaInspiración visual y de ritmoSolo inspiración. No se copió código.

Verificado package.json declara solo cinco dependencias: astro, @astrojs/react, @astrojs/sitemap, react y react-dom. Ninguna librería de animación, de scroll ni de 3D.

Lectura mía

La conclusión de la investigación

Casi todo lo que quería cabe en CSS moderno: animation-timeline, View Transitions y clip-path. WebGL no hacía falta. Cuando lo probé por instinto, el resultado fue peor (véase el cursor fluido en el sistema de movimiento).

Línea de tiempo del proceso

  • 27 a 29 de septiembre: preloader, cursor propio y modo fluido WebGL; este último se retira.
  • 22 de octubre: auditoría de accesibilidad (199 hallazgos), rendimiento, imágenes y migración a Astro.
  • 23 de octubre: capa cinematográfica, menú móvil, clips, separadores, islas React y QA.

04 · Principios de movimiento

Reglas que me puse, y cuánto las cumplí.

Fijé siete reglas antes de implementar. Al revisar el código para este caso comparé cada regla con la realidad. No salgo limpio, y creo que esa tabla es lo más útil del caso.

Cada regla frente a lo que hay en el código a fecha de hoy.
ReglaRealidad en el código
Una animación protagonista por viewportLectura mía Criterio de composición; no tengo una medición automática de ello.
Micro-interacciones de 120 a 200 msSin auditoría sistemática de todas; no afirmo cumplimiento.
Reveals de 400 a 700 msSe cumple en rv-up (.7 s). Excepciones: máscara de titulares 1 s, imágenes de tarjeta .9 s, menú móvil .85 s de apertura.
Escalonado de 40 a 70 ms, máximo 8 elementosTitulares: 70 ms. Excepciones: pastillas del stack 45 ms hasta 20 elementos, filas 28 ms, letras del titular 22 ms hasta 40, pie del menú hasta 1,14 s de retardo.
Sin secuestro de scroll ni snap obligatorioSin scroll-jacking. Hay scroll-snap-type: x mandatory horizontal en las rejillas proof-grid y test-g en móvil (styles.css); testimonios lo sustituye por proximity. Ninguno vertical.
Contenido visible por defectoLas clases que ocultan (sfx-rev) las añade JavaScript solo con no-preference.
Todo bajo prefers-reduced-motionLa QA halló animaciones infinitas sin guarda (corregido en 7315075). Al escribir este caso encontré el tilt 3D de tarjetas sin guarda y lo corregí (128a664).

Los números del escalonado y las duraciones los fijé a partir de mi criterio de diseño; no son un estándar. Lo que sí es verificable es dónde se cumplen y dónde no.

05 · Sistema de movimiento

Nueve piezas, cada una con su ficha.

Cada ficha responde cinco preguntas: qué hace, qué problema resuelve, qué investigué, qué descarté y qué cuesta. Seis incluyen una demo real que puedes probar. Todas se detienen con su botón y, si tu sistema pide menos movimiento, empiezan paradas y las reproduces tú.

01

Animaciones ligadas al scroll

src/motion.css · src/scroll-fx.css · src/previews.css

Qué hace
Barra de progreso (scaleX con scroll(root)), entrada de tarjetas, parallax y zoom lento de imágenes, tinte de sección, línea de separador que se dibuja e inclinación del marco de laptop. Todo con animation-timeline, sin escuchar el evento de scroll.
Problema que resuelve
Lectura mía Orientar (cuánto falta, dónde estoy) y dar profundidad sin ejecutar JavaScript en cada fotograma de scroll.
Investigación
Estudié Lenis y Locomotive Scroll, que resuelven esto con JS. La API nativa cubre el caso sin librería. Detrás de @supports (animation-timeline: scroll()) y de no-preference.
Alternativa descartada
Scroll suave por JS y animaciones con listeners: añaden peso, y el scroll suave por JS mueve el scroll a su ritmo, contra mi regla.
Coste
Rendimiento: solo propiedades que se componen (transform, translate, opacity, clip-path). Imágenes con dos animaciones a la vez (parallax y zoom); Hipótesis en móviles de gama baja puede notarse, no lo he medido. Accesibilidad: sin no-preference o sin soporte del navegador, todo se ve estático y completo. Dos fallos reales: véase la demo y la sección de errores.
Prueba 1 · scroll y el fallo de overflow

Tu sistema pide menos movimiento: la demo empieza detenida. Si la reproduces, lo decides tú.

Tu navegador no soporta animation-timeline: verás las tarjetas estáticas, que es justo el comportamiento de respaldo del sitio.

Tarjeta 1. Desplázate dentro de esta caja.
Tarjeta 2. Cada una entra ligada a su posición, sin JavaScript.
Tarjeta 3. La barra superior avanza con el scroll de la caja.
Tarjeta 4. Cambia a overflow: hidden y vuelve a desplazarte.
Tarjeta 5. Con hidden la sección pasa a ser la fuente del timeline y las tarjetas no animan.
Tarjeta 6. Es el fallo que encontré en .sec y que arreglé con clip.

Qué observar: con clip las tarjetas entran al aparecer. Con hidden, la sección interior se convierte en el scroller del que depende la animación; como ella no se desplaza, el timeline nunca avanza.

Dos separadores de sección: una regla técnica con el número 01 y el nombre Sobre mí, y una marquesina con la palabra STACK en letra contorneada y rellena
Separadores reales del sitio (pieza 06). La línea de entrada de cada sección usa este mismo mecanismo de scroll.
02

Tipografía cinética y entradas de texto

src/main.js (split) · src/cinema.css · src/effects.js · src/scroll-fx.css

Qué hace
El titular de la portada se parte en letras que entran girando 8° desde la base de la palabra (22 ms de retardo por letra, máximo 40 letras). Además: máscara de titulares por clip-path (1 s), números de sección que cuentan de 00 al valor (900 ms) y un scramble de 700 ms solo en los eyebrows.
Problema que resuelve
Lectura mía Marcar con un solo gesto cuál es la primera cosa que hay que leer.
Investigación
Referente: split-text de Active Theory (MIT). Lo reescribí a mano. El código re-divide el texto tras cada cambio de idioma para no romper la traducción.
Alternativa descartada
Scramble en los titulares: llevan <em> y <br> y el cambio de idioma reescribe su HTML, así que quedó limitado a texto plano.
Coste
Rendimiento: un <span> por letra, solo en el titular de la portada. Accesibilidad: Hipótesis partir el texto en spans puede hacer que algunos lectores de pantalla deletreen; el código real no añade aria-label y no lo he probado con lector. La demo sí lo hace bien. Bajo reduced el titular aparece directamente.
Prueba 2 · el escalonado entre letras

Tu sistema pide menos movimiento: la demo empieza detenida. Si la reproduces, lo decides tú.

Qué observar: con 0 ms todo entra de golpe; con más de 70 ms la frase tarda en leerse. Mi valor real es 22 ms.

03

View Transitions entre temas y entre páginas

src/motion.js · src/sections.js · src/islands/store.js

Qué hace
Al cambiar de tema, un círculo (650 ms) crece desde el botón pulsado y revela el tema nuevo con document.startViewTransition y clip-path. Entre el portfolio y los casos hay un fundido de 300 ms con @view-transition.
Problema que resuelve
Lectura mía Un cambio de tema instantáneo parece un fallo; un círculo que nace del botón explica qué ha pasado y de dónde.
Investigación
El tema vive en html[data-theme] y se aplica con un script en línea antes del primer pintado, así no hay destello al cargar.
Alternativa descartada
Lectura mía Animar con transition todas las variables de color: cientos de nodos recalculando a la vez, sin control sobre el orden.
Coste
Rendimiento: una captura de la página por cambio. Hipótesis mientras dura la transición el documento no responde a clics (la capa de captura los intercepta); son 650 ms. Accesibilidad: con reduced o sin soporte del navegador el cambio es instantáneo.
Prueba 3 · cambio de tema con View Transitions

Tu sistema pide menos movimiento: la demo empieza detenida. Si la reproduces, lo decides tú.

Un solo componente, tres temasEl círculo nace en el botón que pulsas.

Qué observar: el origen del círculo cambia con el botón. Con «Pausar efecto» el cambio es instantáneo, como en el sitio con reduced.

04

Menú móvil

src/mobile-menu.css · src/mobile-menu.js

Qué hace
Se abre como un círculo desde la hamburguesa (clip-path, 850 ms), los enlaces suben desde una máscara de línea (70 ms entre enlaces), el fondo tiene tres manchas que se mueven solo mientras está abierto, el foco queda atrapado y se cierra arrastrando.
Problema que resuelve
Lectura mía El menú anterior (un panel con fundido de 380 ms, en git) funcionaba, pero no explicaba de dónde venía. El origen en el botón da causa y efecto.
Investigación
Cierre por gesto con proyección de velocidad: se cierra si el recorrido más la velocidad por 220 supera 130 px. Vibración de 10 ms solo con activación previa del usuario y sin reduced. El commit 2abde80 corrigió tres errores: cancelaba el scroll suave, el clic tras arrastrar activaba enlaces y la vibración se disparaba sin gesto.
Alternativa descartada
Mantener el fundido anterior. Sigue existiendo como versión de reduced: 250 ms de opacidad y nada más.
Coste
Rendimiento: clip-path animado una vez al abrir; las manchas usan will-change y quedan pausadas con el menú cerrado. Contra mi regla: con los retardos del pie, la entrada completa llega a unos 1,1 s. Accesibilidad: la etiqueta del botón cambia entre «Abrir menú» y «Cerrar menú» en es, ca y en, y Escape lo cierra. Hipótesis sin probar con VoiceOver ni TalkBack.
Prueba 4 · apertura circular

Tu sistema pide menos movimiento: la demo usa la versión con fundido. Si la reproduces, lo decides tú.

RN

Qué observar: el círculo nace en la hamburguesa y los enlaces suben de uno en uno. Pulsa Escape o cualquier enlace para cerrar.

Tres capturas del menú móvil real: portada cerrada, círculo del menú abriéndose desde el botón arriba a la derecha y menú abierto
Menú real a 390 px: cerrado, a los 280 ms y abierto.
05

Cursor, tilt y botones magnéticos

src/main.js · src/motion.js · src/cinema.js

Qué hace
Un punto y un halo siguen al puntero y cambian con botones y tarjetas («Ver →»). Un selector permite el modo Mira. Las tarjetas se inclinan hasta 8°. Los botones se acercan al puntero un 25% con tope de 10 px. El hero tiene capas de profundidad que reaccionan al ratón.
Problema que resuelve
Lectura mía Dar respuesta táctil a lo que es pulsable en escritorio. En móvil no existe y no se simula.
Investigación
Referente: mouse-follower de Cuberto (MIT), por los estados del cursor. El historial muestra un camino accidentado: se probó un cursor fluido WebGL (35f7b26 y 08ca8b7) y se retiró (ff45ab5: 1.321 líneas menos, «no funcionaba en producción»). Después, 331b464 bajó el lag del cursor.
Alternativa descartada
El cursor fluido WebGL. Y dentro del cursor actual: el halo con mix-blend-mode: difference en un tema, por ser de las operaciones de composición más pesadas (mismo commit).
Coste
Rendimiento: un solo requestAnimationFrame que se detiene al parar el puntero, y un listener global en vez de uno por elemento. No he medido fotogramas. Accesibilidad: solo con puntero fino y pantalla de más de 1024 px. Pero el cursor del sistema se oculta (cursor: none); Hipótesis puede restar visibilidad a quien lo tenga ampliado. Con reduced el modo inicial del cursor es siempre Normal y el tilt se desactiva (arreglado hoy).
Prueba 5 · botones magnéticos

Tu sistema pide menos movimiento: la demo empieza detenida. Si la reproduces, lo decides tú.

Este efecto necesita ratón o trackpad. En pantallas táctiles el sitio real tampoco lo aplica.

Qué observar: el botón se desplaza hacia el puntero y vuelve con un muelle suave. Mi valor real es 25% con tope de 10 px; con más, el botón se escapa del dedo y parece un fallo.

06

Separadores de sección

src/sections.js · src/sections.css

Qué hace
Ocho separadores (comprobado en el navegador) que alternan una regla técnica numerada (§01, 01/08) con una marquesina de 48 s con el nombre de la sección. Se pausan fuera de pantalla y mientras se hace scroll.
Problema que resuelve
Lectura mía Dar ritmo a una página larga y orientar con el número de sección.
Investigación
Se insertan por JavaScript con aria-hidden, porque cada sección ya tiene su h2. Los nombres se traducen con el idioma.
Alternativa descartada
Lectura mía Una línea estática: más barata, pero la página quedaba plana entre secciones.
Coste
Rendimiento: animación de transform pausada fuera de vista con IntersectionObserver. Accesibilidad, una deuda abierta: WCAG 2.2.2 pide poder pausar un movimiento automático de más de 5 s. La marquesina real no tiene botón de pausa (sí respeta reduced). La demo de abajo sí lo tiene.
Prueba 6 · marquesina con pausa

Tu sistema pide menos movimiento: la demo empieza detenida. Si la reproduces, lo decides tú.

Qué observar: se detiene fuera de pantalla (como en el sitio) y con el botón. La demo va a 24 s por vuelta para que se aprecie; el sitio usa 48 s.

07

Clips de proyectos en marco de laptop y teléfono

src/components/ProjectPreview.astro · src/preview.js · src/previews.css

Qué hace
Cada proyecto destacado muestra su producto en uso dentro de un marco hecho con CSS puro. El vídeo no se pide hasta que entra en pantalla (35% visible) y se pausa al salir o con la pestaña oculta.
Problema que resuelve
Lectura mía Ver la interacción del producto antes de entrar al caso, sin imágenes estáticas que no cuentan nada.
Investigación
Cinco clips WebM de 165 a 392 KB y cinco pósters WebP de 24 a 32 KB. Suman 1,43 MiB, pero cada uno se descarga solo si se ve.
Alternativa descartada
Lectura mía Embeds en vivo o GIFs: pesan mucho más y no se pueden pausar con criterio.
Coste
Rendimiento: con preload="none" el vídeo no cuesta nada al cargar. Con ahorro de datos, red 2g o reduced queda solo el póster. Accesibilidad: el <video> es decorativo (aria-hidden, sin controles) y el póster lleva alt. Misma deuda que la marquesina frente a WCAG 2.2.2.
Tarjeta destacada de Capsule Corp con el clip del proyecto dentro de un marco de laptop y el texto del caso a la derecha
Marco de laptop con el clip en reproducción (captura real).
Tarjeta de ZODI con el clip dentro de un marco de teléfono y el texto del caso a la derecha
Variante de teléfono para los productos móviles.
08

Preloader

src/main.js (bloque Preloader)

Qué hace
Una entrada con el nombre y una línea de progreso que dura 0,9 s (0,4 s con reduced) y se puede saltar con clic, Escape, espacio o Enter.
Problema que resuelve
Lectura mía Una primera impresión de marca que no se come la paciencia de quien llega.
Investigación
Nueve commits con «preloader» en el asunto entre el 27 de septiembre y el 22 de octubre. El comentario del código dice que retrasaba el LCP unos 3 s y por eso se acortó de 2 s a 0,9 s.
Alternativa descartada
El sistema anterior: brillo multi-sombra, halo verde, cortinas, estallido de puntos y rayo de luz. El commit b5d0f7e lo descarta entero por uno minimalista.
Coste
Rendimiento: 0,9 s de espera visible en cada visita (se muestra siempre). Hipótesis para quien vuelve, un preloader siempre visible es fricción; mostrarlo solo la primera vez sería más amable. No lo he cambiado ni lo he medido con usuarios.
09

Sonido y música

src/main.js (Music player) · src/components/Footer.astro · public/audio/

Qué hace
Un botón Activar música en la portada arranca una pista en bucle a volumen 0,38 y muestra un reproductor pequeño (pausa, silencio, cerrar, progreso). Cerrarlo la detiene y la rebobina.
Problema que resuelve
Lectura mía Añadir una capa de personalidad para quien la quiera, sin imponer sonido a quien revisa el portfolio en una oficina o en una reunión.
Investigación
La pista pesa 3.802.443 bytes (unos 3,6 MiB). El <audio> lleva preload="none": no se descarga hasta el primer clic, y ese clic es además el gesto que exigen los navegadores para reproducir.
Alternativa descartada
Reproducir al cargar. Los navegadores lo bloquean y, aunque pudiera, sería hostil. Por eso es opcional y nunca arranca solo (play() solo se llama desde un clic).
Coste
Rendimiento: cero al cargar; 3,6 MiB solo si se activa. Accesibilidad: el botón cambia su etiqueta según el estado (Activar música de fondo, Pausar música, Reanudar música), el reproductor es una región con nombre y hay silencio y cierre. Hipótesis no lo he probado con lector de pantalla. No hay demo aquí a propósito: reproducir audio dentro de un caso sería justo lo que descarté. Pruébalo en el portfolio.

06 · Sistema de 5 temas

Una sola base, cinco personalidades.

El sitio se puede ver en cinco temas con data-theme (saas, editorial, cyber, diario, hud). Cambian tipografía, paleta y carácter; el contenido y la estructura son los mismos.

Portada en el tema Dual Core AI, oscuro con acentos teal y rojo
Dual Core AI · Inter, teal y rojo
Portada en el tema El Diario Digital, papel sepia, serifa y tinta roja
El Diario Digital · Lora, sepia y tinta
Portada en el tema Abstract Cyber, fondo oscuro con neón violeta y cian
Abstract Cyber · Outfit, neón y plasma
Portada en el tema Minimal Journal, blanco con azul
Minimal Journal · Inter, blanco y azul
Portada en el tema Fine Art, negro mate con dorado
Fine Art · Playfair, negro mate y dorado

Qué problema resuelve

Lectura mía Un portfolio visual tiene que demostrar rango. Cinco temas sobre la misma base muestran que el sistema aguanta cambios de tono sin rehacer nada.

Qué costó

Verificado Cada tema multiplica los casos de contraste. La QA encontró fallos reales de contraste en los chips de las tarjetas por tema (66cd877). Lighthouse solo audita el tema por defecto; los demás los revisó la QA con Playwright en tres temas y dos viewports, no en los cinco. Los dos restantes quedan sin revisión automática.

La persistencia usa un script en línea que aplica el tema guardado antes del primer pintado, y el cambio pasa por la View Transition de la pieza 03. Los nombres internos no coinciden con los públicos (editorial es El Diario Digital, diario es Minimal Journal, hud es Fine Art): arrastre de versiones anteriores que no he renombrado.

07 · Accesibilidad y reduced-motion

El movimiento es opcional. El contenido no.

Verificado

Lighthouse

Accesibilidad de 90 a 100 en móvil, en el tema por defecto. Auditoría previa de 199 hallazgos de diseño (703d3cc).

Verificado

Reduced-motion

Los efectos viven detrás de prefers-reduced-motion: no-preference. Los clips quedan en póster, el menú se abre con fundido de 250 ms y el cambio de tema es instantáneo.

Hipótesis

Lo que no he probado

Lectores de pantalla reales (VoiceOver, NVDA, TalkBack), el ocultamiento del cursor del sistema con baja visión y la pausa de las marquesinas (WCAG 2.2.2).

Esta página aplica lo mismo: las demos empiezan paradas si tu sistema lo pide, todas tienen botón de pausa, los controles miden al menos 44 px y el contraste del texto supera 4,5:1 (lo comprobé con un cálculo sobre los colores de esta hoja de estilos).

08 · Rendimiento

De 64 a 96-98, y un diagnóstico equivocado.

Medí con Lighthouse móvil en local sobre la portada. El orden es el de mis mediciones; no conservé los informes, así que no atribuyo cada salto a un commit concreto.

Evolución de las puntuaciones de Lighthouse móvil Rendimiento: 64, 82, 91 y 96 a 98 en cuatro mediciones. Accesibilidad: de 90 a 100. SEO: de 92 a 100. 100908070 60 64829196-98 90100 Medición 1234 Acc. / SEO
■ Rendimiento · ■ Accesibilidad, de 90 a 100 · ■ SEO, de 92 a 100. De accesibilidad y SEO solo tengo inicio y fin, no puntos intermedios. Lighthouse móvil en local, con throttling simulado.
Historia del LCP de la portada El texto del hero se pintaba en unos 0,2 segundos. El LCP simulado era de 4,8 segundos y tras diferir las imágenes quedó entre 2,3 y 3,4 segundos. 0 s1,25 s2,5 s3,75 s4,8 s Texto del heroLCP simulado antesLCP simulado después ~0,2 s 4,8 s 2,3 a 3,4 s
Medición de mi portada. El tramo claro es el rango entre mediciones.
Verificado

El diagnóstico del LCP

Pensé que el titular tardaba en pintarse. Midiendo, el texto salía en unos 200 ms; los 4,8 s eran del simulador de Lighthouse (Lantern), que contaba unos 530 KB de imágenes de tarjetas descargadas antes de load. La solución fue diferirlas al evento load (e5ebd2c), y el LCP simulado bajó a 2,3-3,4 s.

Verificado

Otras palancas

  • Imagen OG de 1.805.249 bytes a JPG de 101.400 y WebP de 36.008 (cbeebe3).
  • Preloader de 2 s a 0,9 s.
  • Fuentes y Font Awesome sin bloquear el render.
  • Imágenes WebP con srcset mediante astro:assets.

Los números de Lighthouse del párrafo anterior son los que me quedaron anotados de mis mediciones; no son un resultado repetible desde este repositorio. Las cifras de tamaño sí lo son: salen de git y de la compilación.

09 · Arquitectura

De Vite a Astro, con islas React solo donde pesan.

El 22 de octubre migré de una página única con Vite a Astro (369e9b5): componentes por sección, subpáginas servidas desde public/, imágenes WebP con srcset, sitemap automático. Al día siguiente, tres piezas con estado compartido pasaron a islas React con client:idle (2a231b2): el tema, el idioma y el filtro de proyectos. El resto sigue en CSS y JS sin framework.

Comparación honesta de las opciones consideradas.
OpciónCoste en JSQué ganoQué pierdo
Todo vanilla0 KB extraNada que hidratarEl estado de tema, idioma y filtro se reparte entre el DOM y varios manejadores. Más difícil de mantener.
Astro con islas React (elegida)+72 KB gzip medidosUna fuente de verdad (html[data-theme] y un store) compartida por el dock, el menú y el selector72 KB para tres componentes pequeños
SPA React completaNo medidoModelo únicoLectura mía Desproporcionada para un sitio de contenido estático.

Verificado Medido hoy tras npm run build, sumando el cliente de React (65,8 KB gzip), react (3,0), jsx-runtime (1,1) y las tres islas (2,4): unos 72 KB gzip. El script principal del sitio, ya existente, pesa 26,8 KB gzip.

Lectura mía Es una decisión de mantenimiento, no de rendimiento: la ganancia no está en la velocidad sino en no repetir el estado. Hipótesis client:idle evita que la hidratación compita con el primer pintado; la puntuación de rendimiento no cayó tras añadirlas, pero no aislé su efecto con un A/B.

10 · Agentes y QA

Dirigir agentes es diseñar un proceso.

Trabajé con agentes especializados: uno investigaba, otros implementaban en ramas aisladas (worktrees) y otros revisaban. Mi trabajo fue decidir qué se pedía, fijar los criterios y revisar lo que volvía.

Verificado

En números

45 commits entre el 22 y el 23 de octubre, 10 de ellos fusiones de ramas de agentes. 37 llevan el trailer Co-Authored-By de Claude.

Verificado

Fallos reales de la QA

  • Contraste de los chips por tema.
  • El dock en móvil tapaba texto.
  • Animaciones infinitas sin guarda de reduced.
  • Un título sin traducir.
Lectura mía

Lo que aprendí

Un agente que implementa no es el que debe revisar. Los fallos más útiles los halló la QA con Playwright, a 1440 y 390 px, no la implementación original.

Herramientas que sí se usaron en este sitio y dónde queda su rastro en el repositorio.
HerramientaPara qué se usóEvidencia
Claude CodeImplementación y revisión por agentes que yo dirigíaTrailer Co-Authored-By en los commits (git log)
Git worktreesCada subagente trabaja en una rama aislada y se fusiona despuésFusiones worktree-agent-* en el historial
CLAUDE.md y graphifyReglas del proyecto y grafo de conocimiento del código para los agentesCLAUDE.md en la raíz. El grafo no se versiona
ImpeccableDetector de calidad de diseño; triage de 199 hallazgos.impeccable/config.json (703d3cc)
PlaywrightQA a 1440 y 390 px y capturas reales de demosMensaje del commit 24a9cee (capturas); QA descrita en esta sección
axe-core y LighthouseAccesibilidad y rendimiento móvil, en localMensaje del commit 2cd6ea0; mediciones de la sección 08
HyperFramesProyecto base de un reel (HTML a vídeo), fuera del buildvideo/reel/ (1df5500). Solo el proyecto base: sin render publicado
RemotionTeaser original de Capsule Corp mostrado en una tarjetaCommit 692afe1
Vercel y GitHubDespliegue y ramasvercel.json (c5e8238); remoto origin
Límite de mi QA. Se probó en tres temas y dos viewports, no en los cinco temas ni en dispositivos reales.

11 · Errores y aprendizajes

Lo que salió mal, y qué cambié.

Verificado

1 · overflow:hidden mataba los timelines

.sec{overflow:hidden} convertía la sección en la fuente del view(); como no se desplaza, ninguna animación interior corría. Cambié a overflow:clip (a9f6eee). La demo 1 lo reproduce.

Verificado

2 · El minificador fusionaba animation

lightningcss unía animation y animation-timeline en un shorthand que algunos Chrome descartan. Escribo longhands (d382e44, e794ac8). Lo anoto en el CSS para no repetirlo.

Verificado

3 · Culpé al texto de un LCP que era de imágenes

Tardé en medir el LCP real: el texto salía en 200 ms. El problema eran las imágenes antes de load y el simulador.

Verificado

4 · Cursor fluido WebGL

Dos commits para añadirlo, uno para quitarlo: 1.321 líneas fuera. La técnica era correcta; el resultado en producción no.

Verificado

5 · Reglas que yo mismo rompí

La tabla de principios muestra escalonados y duraciones fuera de mi rango, y un tilt sin guarda de reduced que descubrí al escribir este caso.

Verificado

6 · Nueve commits de preloader

Iterar la entrada del sitio costó nueve commits y una decisión final de simplificar. El gesto más pequeño tenía el coste más alto.

12 · Qué mediría y qué falta validar

Lo que no sé todavía.

No tengo datos de uso ni he probado el sitio con personas. Todo lo que sigue son hipótesis y cómo las comprobaría.

Hipótesis

El movimiento ayuda a recordar el sitio

Lo comprobaría con 5 o 6 sesiones moderadas de personas que revisan portfolios: tiempo hasta encontrar un caso y qué recuerdan después. Sin cifras previas.

Hipótesis

El preloader es fricción para quien vuelve

Mediría la tasa de saltarlo (clic o tecla) si añadiera un contador de eventos propio. Hoy no existe.

Hipótesis

El coste en móviles de gama baja es aceptable

Grabaría trazas de rendimiento en un Android modesto durante el scroll y mediría INP y tareas largas, no solo Lighthouse.

Hipótesis

El modo accesible funciona con tecnologías de apoyo

Probaría con VoiceOver, NVDA y TalkBack el titular dividido, el menú, el reproductor y la pausa de las marquesinas.

Mejoras que haría ahora

  • Añadir un control de pausa a la marquesina y a los clips para cumplir WCAG 2.2.2.
  • Mostrar el preloader solo la primera vez por sesión.
  • Revisar contraste en los cinco temas con una herramienta automática, no solo en tres.
  • Renombrar los identificadores de tema para que coincidan con sus nombres públicos.

Mira el código

Todo lo que he contado se puede comprobar.

El repositorio es público: el historial de git, los commits que cito y el código de cada pieza. Y si te interesa otra forma de entender mi criterio, Capsule Corp convierte mi CV en un juego.