Saltar al contenido

El Problema: La Tiranía de la Recarga de Página

Como comentaba en Mi Odisea de 25 Años hacia una Web Instantánea, mi viaje de 25 años por el desarrollo web me ha llevado a una conclusión fundamental: la mayoría de las arquitecturas modernas luchan contra el enemigo equivocado.

El Falso Grial: El Renderizado en Servidor (SSR)

Cuando empecé a oír hablar de SSR en el ecosistema de Nuxt 3, me sonaba a magia (al final resulto casi en lo que había estando haciendo 1 decada y media). La idea de que el servidor enviara un HTML completamente renderizado prometía un Time To First Byte (TTFB) y un First Contentful Paint (FCP) espectaculares. Y lo cumple. Para la primera visita de un usuario desde Google, el SSR es fantástico.

Pero el diablo está en los detalles, concretamente en el ancho de banda. En un modelo SSR tradicional, cada recarga de página (F5), cada navegación a una nueva sección, implica descargar de nuevo un documento HTML completo (16KB, 60KB, o más). Esto, en una red 3G, es una eternidad.

Aquí es donde entra en juego la arquitectura PWA con "App Shell", y es crucial entender su "trade-off":

  • El Coste Inicial: Sí, la primera vez que un usuario visita el sitio (y cada vez que hay una actualización), tiene que descargar el "cascarón" completo de la aplicación (los assets JS/CSS/fuentes).
  • El Retorno de la Inversión: Aquí es donde el caso de uso lo cambia todo.
    • Si tu sitio es un blog que recibe 100 usuarios que lo visitan una vez y no vuelven, el coste inicial del SW puede no compensar. El SSR tradicional podría ser más eficiente en ese escenario.
    • Pero si tu aplicación es una herramienta que 10 usuarios visitan 300 veces al día, el cálculo se invierte drásticamente. Pagan el "peaje" de la descarga una sola vez. A partir de ahí, las 299 visitas restantes del día no descargan 60KB de HTML; se sirven instantáneamente desde la caché local. El ahorro de ancho de banda y la mejora en la velocidad percibida son de órdenes de magnitud.

Frameworks como Nuxt o Next.js son excelentes para la navegación híbrida, pero el problema fundamental de la recarga de página completa persiste. Nuestra arquitectura no solo optimiza la navegación en el cliente; ataca de raíz el coste de la recarga para el usuario recurrente.

El Verdadero Enemigo: La Red

Frameworks como Nuxt, con sus composables useFetch y useAsyncData, o Next.js con getServerSideProps/getStaticProps y los Server Components, han perfeccionado la navegación híbrida. Esta técnica permite que las transiciones dentro de la app se hagan en el cliente, pidiendo solo el JSON necesario, lo que las hace muy rápidas.

Pero esta navegación rápida en el cliente no es magia exclusiva de estos frameworks. Una SPA "pura y dura" bien construida hace exactamente lo mismo: intercepta los clics en los enlaces y pide solo los datos que necesita.

El problema real, el que estos helpers no solucionan, es la recarga de página completa (F5) o la primera visita a una página profunda. Ahí, la red vuelve a ser la jefa, y nos obliga a descargar de nuevo todo el documento HTML.

La Solución: Una Arquitectura de 3 Fases

La solución a este problema no es gratis, requiere una arquitectura deliberada, al igual que orquestar un SSR correctamente. La estrategia consiste en atacar la latencia de red en tres frentes, pasando de un "Feedback Instantáneo" a un "Renderizado Instantáneo".

Fase 1: Feedback Instantáneo (SSG + SPA)

La base de todo. Usamos Vite-SSG para generar un sitio estático. La primera carga es un HTML ultra-rápido. Una vez cargado, la aplicación se "hidrata" y se convierte en una SPA. Las navegaciones internas son rápidas porque solo piden datos. Para que esto funcione, es crucial separar la lógica de cliente y servidor usando componentes como <ClientOnly> para las partes de la UI que son puramente interactivas.

NOTA

Yo uso Vue y vite-ssg, pero esta arquitectura es agnóstica al framework. Puedes implementarla con SvelteKit o Nuxt o cualquier otro stack moderno que soporte SSG.

Fase 2: Feedback Rápido (PWA con Precaché de Assets)

El refresco de página sigue siendo lento porque, aunque el HTML sea estático, el navegador tiene que volver a descargar todos los assets (JS, CSS, fuentes). Aquí es donde entra el Service Worker. Al convertir la app en una PWA, el SW crea una precaché con todos los assets. En una recarga de página, los recursos se sirven instantáneamente desde la caché local del SW, mejorando drásticamente el LCP.


El Talón de Aquiles de la SPA Moderna: El import() Dinámico

El Service Worker de la Fase 2 no solo acelera la recarga, sino que soluciona el problema más grave y silencioso de las aplicaciones modernas: la fragilidad de la carga perezosa (lazy loading).

Cualquier framework moderno (Vue, React, Svelte...) se basa en la división de código (code splitting) a través de import() dinámicos. En lugar de enviar un gigantesco fichero JavaScript, le enviamos un núcleo pequeño y vamos pidiendo los "trozos" (rutas, diálogos, etc.) a medida que se necesitan.

En una red de fibra, esto es una maravilla. Pero en el mundo real, esta arquitectura es increíblemente frágil. Si la red se cae y el usuario hace clic en algo que necesita un nuevo "trozo" de JavaScript, la petición falla y la aplicación se rompe.

La PWA convierte esta operación frágil en un chaleco antibalas. Como el Service Worker ya ha descargado todos los "trozos" en su precaché durante la instalación, cuando la aplicación pide uno, el SW lo intercepta y lo sirve instantáneamente desde la caché local, sin depender de la red. No es una optimización, es una garantía de que la aplicación nunca se romperá por un fallo de red al cargar sus propios módulos.


Fase 3: Renderizado Instantáneo (Arquitectura "App Shell" Resiliente)

Esta es la estocada mortal a la latencia, pero requiere una coreografía precisa. Tu Service Worker implementa una lógica mucho más inteligente que un simple "servir desde caché":

  1. En una recarga, el SW intercepta la petición y lanza dos tareas en paralelo con Promise.allSettled: a. Cargar el "cascarón" HTML (sin datos) desde su caché local. b. Pedir el JSON de estado a un endpoint /state-api en la red.
  2. El SW espera a ambas. Aquí reside la resiliencia:
    • Si la petición al /state-api devuelve una respuesta inesperada (un error HTML del servidor, una redirección por autenticación, etc.), el SW actúa como un "proxy" inteligente: descarta el cascarón de la caché y sirve la respuesta del servidor, respetando siempre su autoridad.
    • Si la petición al /state-api falla (porque estamos offline), el SW sirve el cascarón de la caché con un estado vacío ({}), permitiendo que la aplicación muestre su UI de "sin conexión".
    • Si ambas promesas se cumplen con éxito, el SW realiza la "costura" (stitching): inyecta el pequeño JSON dentro del cascarón HTML y sirve la página completa e hidratada al instante.

El resultado es una aniquilación del payload de red en el caso feliz. Pasamos de transferir 16KB de HTML a solo unos cientos de bytes de JSON. Es esta reducción drástica del tráfico de red, combinada con una lógica de fallbacks robusta, lo que hace que la aplicación se sienta instantánea.

Liberado bajo la licencia MIT