Service workers y el modelo de shell de la aplicación

Una función arquitectónica común de las aplicaciones web de una sola página (SPA) es un conjunto mínimo de HTML, CSS y JavaScript que se necesitan para impulsar la funcionalidad global de una aplicación. En la práctica, esto suele ser el encabezado, la navegación y otros elementos comunes de la interfaz de usuario que persisten en todas las páginas. Cuando un service worker almacena previamente en caché el HTML mínimo de la IU y los recursos dependientes, se denomina shell de aplicación.

Diagrama de una shell de aplicación. Es una captura de pantalla de una página web con un encabezado en la parte superior y un área de contenido en la parte inferior. El encabezado tiene la etiqueta "Application Shell", mientras que la parte inferior está etiquetada como "Content".

La shell de app desempeña un papel importante en el rendimiento percibido de una aplicación web. Es lo primero que se carga y, por lo tanto, también es lo primero que ven los usuarios mientras esperan que el contenido se propague en la interfaz de usuario.

Si bien la shell de app se carga rápidamente (siempre que la red esté disponible y al menos un poco rápida), un service worker que almacena previamente en caché el shell de la aplicación y sus elementos asociados le otorga al modelo de shell de app estos beneficios adicionales:

  • Rendimiento confiable y coherente en visitas repetidas En la primera visita a una app sin un service worker instalado, el lenguaje de marcado de la aplicación y sus elementos asociados deben cargarse desde la red antes de que el service worker pueda colocarlos en su caché. Sin embargo, las visitas repetidas extraerán la shell de la app del caché, lo que significa que la carga y el procesamiento serán instantáneos.
  • Acceso confiable a funciones sin conexión. A veces, el acceso a Internet es irregular o no está presente, y la aterradora pantalla "no podemos encontrar ese sitio web" le cueste a ti. El modelo de shell de aplicación lo soluciona respondiendo a cualquier solicitud de navegación con el lenguaje de marcado de la shell de aplicación desde la caché. Incluso si alguien visita una URL en tu app web a la que nunca visitó, el shell de la aplicación se entregará desde la caché y se podrá propagar con contenido útil.

Cuándo se debe usar el modelo de shell de la aplicación

Una shell de app tiene más sentido cuando tienes elementos comunes de la interfaz de usuario que no cambian de una ruta a otra, pero el contenido sí. Es probable que la mayoría de las SPA usen lo que ya es un modelo de shell de aplicación.

Si tu proyecto es así y quieres agregar un service worker para mejorar su confiabilidad y rendimiento, el shell de la aplicación debe hacer lo siguiente:

  • Carga rápida.
  • Usa elementos estáticos de una instancia de Cache.
  • Incluye elementos comunes de la interfaz, como un encabezado y una barra lateral, separados del contenido de la página.
  • Recupera y muestra contenido específico de la página.
  • Si corresponde, puedes almacenar en caché el contenido dinámico para reproducirlo sin conexión.

La shell de la aplicación carga contenido específico de la página de forma dinámica a través de APIs o contenido empaquetado en JavaScript. También debe actualizarse, ya que si cambia el lenguaje de marcado del shell de la aplicación, la actualización de un service worker debe captar la nueva shell de app y almacenarla en caché automáticamente.

Cómo compilar la shell de la aplicación

La shell de la aplicación debe existir independientemente del contenido, pero debe proporcionar una base para que el contenido se propague en su interior. Lo ideal sería que sea lo más delgado posible, pero debería incluir suficiente contenido significativo en la descarga inicial para que el usuario comprenda que la experiencia se carga rápidamente.

El equilibrio adecuado depende de tu app. La shell de app para la app de Trained To Thrill de Jake Archibald incluye un encabezado con un botón de actualización que permite obtener contenido nuevo de Flickr.

Captura de pantalla de la app web Trained to Thrill en dos estados diferentes. A la izquierda, solo es visible la shell de la aplicación almacenada en caché, sin contenido propagado. A la derecha, el contenido (algunas imágenes de algunos trenes) se carga de forma dinámica en el área de contenido del shell de la aplicación.

El lenguaje de marcado de la shell de aplicación variará de un proyecto a otro, pero a continuación te mostramos un ejemplo de un archivo index.html que proporciona el código estándar de la aplicación:

​​<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>
      Application Shell Example
    </title>
    <link rel="manifest" href="/manifest.json">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="stylesheet" type="text/css" href="styles/global.css">
  </head>
  <body>
    <header class="header">
      <!-- Application header -->
      <h1 class="header__title">Application Shell Example</h1>
    </header>

    <nav class="nav">
      <!-- Navigation items -->
    </nav>

    <main id="app">
      <!-- Where the application content populates -->
    </main>

    <div class="loader">
      <!-- Spinner/content placeholders -->
    </div>

    <!-- Critical application shell logic -->
    <script src="app.js"></script>

    <!-- Service worker registration script -->
    <script>
      if ('serviceWorker' in navigator) {
        // Register a service worker after the load event
        window.addEventListener('load', () => {
          navigator.serviceWorker.register('/sw.js');
        });
      }
    </script>
  </body>
</html>

Sin embargo, cuando construyas un shell de aplicación para tu proyecto, este debe tener las siguientes características:

  • El código HTML debe tener áreas claramente aisladas para los elementos individuales de la interfaz de usuario. En el ejemplo anterior, esto incluye el encabezado de la aplicación, la navegación, el área de contenido principal y el espacio para un "ícono giratorio" de carga que solo aparece cuando se está cargando el contenido.
  • El código JavaScript y CSS inicial que se carga para la shell de la aplicación debe ser mínimo y solo debe relacionarse con la funcionalidad de la shell de la aplicación en sí y no con el contenido. Esto garantiza que la aplicación renderice su shell lo más rápido posible y minimiza el trabajo del subproceso principal hasta que aparezca el contenido.
  • Una secuencia de comandos intercalada que registra un service worker.

Una vez que se haya compilado el shell de la aplicación, puedes compilar un service worker para almacenar en caché tanto dicho shell como sus elementos.

Cómo almacenar en caché la shell de la aplicación

El shell de la aplicación y sus elementos requeridos son los que el service worker debe almacenar previamente en caché inmediatamente en el momento de la instalación. Suponiendo que se trata de una shell de aplicación como la del ejemplo anterior, veamos cómo se puede lograr esto en un ejemplo básico de Workbox mediante workbox-build:

// build-sw.js
import {generateSW} from 'workbox-build';

// Where the generated service worker will be written to:
const swDest = './dist/sw.js';

generateSW({
  swDest,
  globDirectory: './dist',
  globPatterns: [
    // The necessary CSS and JS for the app shell
    '**/*.js',
    '**/*.css',
    // The app shell itself
    'shell.html'
  ],
  // All navigations for URLs not precached will use this HTML
  navigateFallback: 'shell.html'
}).then(({count, size}) => {
  console.log(`Generated ${swDest}, which precaches ${count} assets totaling ${size} bytes.`);
});

Esta configuración almacenada en build-sw.js importa el CSS y JavaScript de la app, incluido el archivo de lenguaje de marcado de la shell de la aplicación que se encuentra en shell.html. La secuencia de comandos se ejecuta con un nodo de la siguiente manera:

node build-sw.js

El service worker generado se escribe en ./dist/sw.js y registra el siguiente mensaje cuando finaliza:

Generated ./dist/sw.js, which precaches 5 assets totaling 44375 bytes.

Cuando se carga la página, el service worker almacena en caché el lenguaje de marcado de la shell de app y sus dependencias:

Captura de pantalla del panel de red en las Herramientas para desarrolladores de Chrome que muestra una lista de recursos descargados de la red. Los recursos que el service worker almacena previamente en caché se distinguen de los demás recursos con un engranaje a la izquierda de la fila. En el momento de la instalación, el service worker almacena previamente en caché varios archivos JavaScript y CSS.
El service worker almacena previamente en caché las dependencias de la shell de aplicación al momento de la instalación. Las solicitudes de almacenamiento previo en caché son las últimas dos filas, y el ícono de ajustes junto a la solicitud indica que el service worker controló la solicitud.

El almacenamiento previo en caché del HTML, CSS y JavaScript de tu shell de aplicación es posible en casi cualquier flujo de trabajo, incluidos los proyectos que usan agrupadores. A medida que avances en la documentación, aprenderás a usar Workbox directamente para configurar tu cadena de herramientas y compilar un service worker que funcione mejor para tu proyecto, independientemente de si se trata de una SPA.

Conclusión

La combinación del modelo de shell de la aplicación con un service worker es excelente para el almacenamiento en caché sin conexión, en especial si combinas su funcionalidad de almacenamiento previo en caché con una estrategia centrada en la red y de recurrir a la caché para respuestas de API o lenguaje de marcado. El resultado es una experiencia confiable y rápida que renderizará al instante tu shell de app en visitas repetidas, incluso en condiciones sin conexión.