Transiciones sin ser una SPA
Qué argumento arquitectónico desaparece con las transiciones entre documentos, qué sigue justificando una aplicación de página única, y cómo se monta un sitio así.
La razón por la que muchísimos sitios de contenido acabaron siendo aplicaciones de página única no fue técnica: fue que los clientes querían transiciones entre páginas y la única forma de darlas era no cambiar de página. Con @view-transition, ese argumento se ha caído. Esta lección cierra el nivel poniendo la decisión sobre la mesa con honestidad, incluida la parte donde una SPA sigue siendo la respuesta correcta.
- Enumerar qué recupera un sitio al volver a navegación de documentos.
- Distinguir los casos donde una aplicación de página única sigue justificada.
- Montar un sitio de varias páginas con transiciones completas.
- Anticipar las limitaciones que quedan sin resolver.
Lo que se recupera
Volver a navegación normal no es solo quitar código: es devolverle al navegador un montón de trabajo que hace mejor que cualquier router de cliente.
El historial y el botón de atrás funcionan de verdad, incluidas las restauraciones de scroll, la caché de retroceso y las combinaciones raras de pestañas y ventanas. Ningún router de cliente reproduce eso al completo; todos reproducen la parte fácil.
La gestión del foco es la del navegador. Al navegar entre documentos, el foco vuelve al principio del documento nuevo y los lectores de pantalla anuncian la página. En una aplicación de página única eso hay que implementarlo a mano, y la mayoría no lo hace o lo hace mal.
No hay hidratación. El HTML llega renderizado y es interactivo en cuanto se parsea. Desaparece el intervalo entre “se ve” y “responde”, que es la causa de la mitad de las quejas de lentitud en sitios modernos.
El estado de carga es el del navegador. La barra de progreso, el indicador de la pestaña, el cursor. Todo eso es gratis y consistente con el resto del sistema operativo del usuario.
El código que no se escribe no tiene bugs. Un router, un gestor de estado de navegación, una capa de precarga y una gestión de errores de red son varios miles de líneas que dejan de existir.
Lo que sigue justificando una SPA
Sería deshonesto presentar esto como si la decisión fuera obvia. Hay tres situaciones donde una aplicación de página única sigue siendo la respuesta correcta, y ninguna tiene que ver con la estética.
Estado no guardado que debe sobrevivir a la navegación. Un editor con cambios sin guardar, un formulario largo a medias, un carrito que se construye mientras se navega. Cambiar de documento destruye ese estado, y reconstruirlo desde almacenamiento es más frágil que no destruirlo.
Conexiones vivas. Una llamada de voz, una sesión de colaboración en tiempo real, un flujo de datos por socket. Una navegación entre documentos corta la conexión y volver a establecerla no siempre es transparente.
Interacción continua durante la navegación. Un reproductor de música o de vídeo que debe seguir sonando mientras el usuario explora. Es el caso donde no hay alternativa: el documento se destruye y con él el reproductor.
Hay una cuarta razón que la gente da y que ya no se sostiene: “es que necesitamos que sea rápido”. Un sitio de HTML estático servido desde una red de distribución es más rápido que casi cualquier aplicación de página única, porque no tiene que descargar y ejecutar un framework antes de mostrar nada. La velocidad dejó de ser un argumento a favor de las SPA hace bastante tiempo.
Un sitio completo
Poniendo junto todo el nivel, así se monta un sitio de varias páginas con transiciones completas. Todo lo que sigue va en el CSS y el JavaScript comunes a todas las páginas.
@view-transition {
navigation: auto;
}
/* Elementos persistentes: no deben parpadear entre paginas */
header { view-transition-name: vt-cabecera; }
nav { view-transition-name: vt-nav; }
footer { view-transition-name: vt-pie; }
main { view-transition-name: vt-main; }
/* Ritmo comun */
html:active-view-transition {
&::view-transition-group(*) {
animation-duration: 280ms;
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
}
/* La cabecera y el pie no cambian: sin fundido, solo geometria */
::view-transition-old(vt-cabecera),
::view-transition-new(vt-cabecera),
::view-transition-old(vt-pie),
::view-transition-new(vt-pie) {
animation: none;
mix-blend-mode: normal;
}
/* El contenido se desliza segun el sentido */
html:active-view-transition-type(adelante) {
&::view-transition-old(vt-main) { animation: salir 180ms ease-in both; }
&::view-transition-new(vt-main) { animation: entrar 240ms 80ms ease-out both; }
}
html:active-view-transition-type(atras) {
&::view-transition-old(vt-main) { animation: salir 180ms ease-in reverse both; }
&::view-transition-new(vt-main) { animation: entrar 240ms 80ms ease-out reverse both; }
}
@keyframes salir { to { transform: translateX(-2rem); opacity: 0; } }
@keyframes entrar { from { transform: translateX(2rem); opacity: 0; } }
/* Movimiento reducido: el cambio sigue siendo atomico, sin desplazamiento */
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 1ms;
}
}
const ORDEN = ['/', '/blog/', '/proyectos/', '/sobre-mi/'];
function sentido(desdeUrl, hastaUrl) {
const i = ORDEN.indexOf(new URL(desdeUrl).pathname);
const j = ORDEN.indexOf(new URL(hastaUrl).pathname);
if (i === -1 || j === -1) return 'salto';
return j > i ? 'adelante' : 'atras';
}
window.addEventListener('pageswap', (e) => {
if (!e.viewTransition || !e.activation.from) return;
e.viewTransition.types.add(
sentido(e.activation.from.url, e.activation.entry.url)
);
});
window.addEventListener('pagereveal', (e) => {
if (!e.viewTransition || !navigation.activation.from) return;
e.viewTransition.types.add(
sentido(navigation.activation.from.url, location.href)
);
});
Sesenta líneas de CSS y veinte de JavaScript, iguales en todas las páginas, sin router y sin dependencias. En Chromium y Safari se comporta como una aplicación de página única bien hecha. En Firefox, las navegaciones son navegaciones normales y no falta nada.
Fíjate en la capa de movimiento reducido: en lugar de animation: none, usa una duración de un milisegundo. La diferencia importa. Con animation: none sobre los grupos, algunos motores pueden dejar el pseudo-elemento en un estado sin transformación aplicada durante el frame en que existe; con una duración mínima, la animación ocurre igual pero es imperceptible, y el resultado es más predecible entre motores.
Sería fácil terminar el nivel con el entusiasmo de que ya no hacen falta las SPA, y hay tres huecos que conviene tener presentes para no descubrirlos a mitad de proyecto. El primero: no hay forma de precargar el destino y transicionar sin esperar. El navegador tiene que descargar el documento nuevo, y aunque las reglas de especulación pueden traerlo antes de que el usuario pulse, la transición sigue dependiendo de que esté listo; no hay un mecanismo dentro de la API de transiciones para decir “empieza a animar mientras llega”. El segundo: la transición es una caja negra en cuanto empieza. No puedes interrumpirla a mitad para responder a un gesto del usuario, no puedes hacerla reversible según el arrastre, y no puedes atarla a un scroll. Todo el gesto de “arrastrar para volver atrás” de las aplicaciones nativas, que es probablemente la interacción de navegación mejor resuelta que existe, no es expresable. El tercero, y el más práctico: el estado de la interfaz que no está en el DOM se pierde. Un menú desplegado, una posición de scroll dentro de un panel interior, una pestaña seleccionada. Todo eso vuelve a su valor por defecto en el documento nuevo, y sostenerlo obliga a serializarlo en la URL o en almacenamiento, que es exactamente el tipo de trabajo que las SPA hacen por ti. Ninguno de los tres es un argumento para no adoptar las transiciones entre documentos —el modo de fallo es intachable y el coste es dos líneas—, pero sí son los tres puntos donde una aplicación de página única sigue siendo cualitativamente distinta, y merecen estar sobre la mesa cuando alguien pregunte si se puede migrar.