Carga instantánea de apps web con una arquitectura de shell de aplicación

Un shell de aplicación es la mínima cantidad de HTML, CSS y JavaScript necesarios para impulsar una interfaz de usuario. La shell de la aplicación debe cumplir con los siguientes requisitos:

  • carga rápida
  • almacenar en caché
  • mostrar contenido de forma dinámica

Una shell de aplicación es el secreto para obtener un buen rendimiento confiable. Piensa en la shell de tu app como el paquete de código que publicarías en una tienda de aplicaciones si compilaras una app nativa. Es la carga necesaria para poner en marcha, pero es posible que no sea todo. Mantiene tu IU local y extrae contenido de forma dinámica a través de una API.

Separación de shell de aplicación del shell de HTML, JS y CSS, y del contenido HTML

Información general

En el artículo de Alex Russell sobre Apps web progresivas, se describe cómo una app web puede cambiar progresivamente a través del uso y el consentimiento del usuario para proporcionar una experiencia similar a la de una app nativa, con soporte sin conexión, notificaciones push y la posibilidad de agregarse a la pantalla principal. Depende en gran medida de los beneficios de funcionalidad y rendimiento del service worker y sus capacidades de almacenamiento en caché. De esta manera, puedes concentrarte en la velocidad y brindar a tus aplicaciones web la misma carga instantánea y las actualizaciones regulares que acostumbras ver en las aplicaciones nativas.

Para aprovechar al máximo estas capacidades, necesitamos una nueva forma de pensar en los sitios web: la arquitectura de shell de aplicación.

Analicemos cómo estructurar tu app con una arquitectura de shell de aplicación aumentada por service worker. Analizaremos la renderización del cliente y del servidor, y compartiremos una muestra de extremo a extremo que puedes probar hoy mismo.

Para enfatizar este punto, el siguiente ejemplo muestra la primera carga de una app que usa esta arquitectura. Observa el aviso que indica que la app está lista para usarse sin conexión en la parte inferior de la pantalla. Si más adelante hay una actualización de la shell disponible, podemos informarle al usuario que la actualice para obtener la versión nueva.

Imagen de un service worker que se ejecuta en Herramientas para desarrolladores para el shell de la app

¿Qué son los service workers?

Un service worker es una secuencia de comandos que se ejecuta en segundo plano y es independiente de tu página web. Responde a eventos, incluidas las solicitudes de red realizadas desde las páginas que publica y notificaciones push desde tu servidor. Un service worker tiene una vida útil intencionalmente corta. Se activa cuando recibe un evento y se ejecuta solo durante el tiempo necesario para procesarlo.

Los service workers también tienen un conjunto limitado de APIs en comparación con JavaScript en un contexto de navegación normal. Este es un estándar para los trabajadores de la Web. Un service worker no puede acceder al DOM, pero puede acceder a elementos como la API de Cache y puede realizar solicitudes de red con la API de Fetch. La API de IndexedDB y postMessage() también están disponibles para usarse en la persistencia de datos y la mensajería entre el service worker y las páginas que controla. Los eventos de envío enviados desde tu servidor pueden invocar la API de notificaciones para aumentar la participación de los usuarios.

Un service worker puede interceptar solicitudes de red realizadas desde una página (lo que activa un evento de recuperación en el service worker) y mostrar una respuesta recuperada de la red, recuperada de una caché local o, incluso, construida de manera programática. Efectivamente, es un proxy programable en el navegador. La mejor parte es que, independientemente de dónde provenga la respuesta, parece que la página web no está involucrada con un service worker.

Para obtener más información sobre los service workers, lee una introducción a los service workers.

Beneficios de rendimiento

Los service workers son potentes para el almacenamiento en caché sin conexión, pero también ofrecen ventajas significativas de rendimiento mediante la carga instantánea para visitas repetidas a tu sitio o app web. Puedes almacenar en caché la shell de tu aplicación para que funcione sin conexión y complete su contenido con JavaScript.

En visitas repetidas, esto te permite obtener píxeles significativos en la pantalla sin la red, incluso si tu contenido eventualmente proviene de allí. Considéralo como una barra de herramientas y tarjetas que se muestran de inmediato y que el resto del contenido se carga de forma progresiva.

Para probar esta arquitectura en dispositivos reales, ejecutamos nuestra muestra de shell de aplicación en WebPageTest.org y mostramos los resultados a continuación.

Prueba 1: Prueba con un cable Nexus 5 mediante Chrome Dev

La primera vista de la app debe recuperar todos los recursos de la red y no logra una pintura significativa hasta que transcurra 1.2 segundos. Gracias al almacenamiento en caché del service worker, nuestra visita repetida logra una pintura significativa y termina de cargarse por completo en 0.5 segundos.

Diagrama de pintura de prueba de la página web para la conexión de cables

Prueba 2: Prueba 3G con un Nexus 5 usando Chrome Dev

También podemos probar nuestro ejemplo con una conexión 3G un poco más lenta. Esta vez, tarda 2.5 segundos en la primera visita para realizar nuestra primera pintura significativa. La página tarda 7.1 segundos en cargarse por completo. Con el almacenamiento en caché del service worker, nuestra visita repetida logra una pintura significativa y termina de cargarse por completo en 0.8 segundos.

Diagrama de pintura de prueba de la página web para la conexión 3G

Otras vistas cuentan una historia similar. Compara los 3 segundos que tarda en lograr la primera pintura significativa en la shell de la aplicación:

Cronograma de procesamiento de imagen para la primera vista desde la prueba de página web

a los 0.9 segundos que tarda cuando se carga la misma página desde la caché de nuestro service worker. Ahorramos más de 2 segundos de tiempo para nuestros usuarios finales.

Cronograma de procesamiento de imagen para la vista repetida de la prueba de página web

Puedes obtener beneficios de rendimiento similares y confiables para tus propias aplicaciones si usas la arquitectura de shell de aplicación.

¿El service worker requiere que reconsideremos la forma en que estructuramos las apps?

Los service workers suponen algunos cambios sutiles en la arquitectura de las aplicaciones. En lugar de comprimir toda la aplicación en una cadena HTML, puede resultar beneficioso realizar acciones del estilo AJAX. Aquí es donde tienes un shell (que siempre se almacena en caché y siempre puede iniciarse sin la red) y contenido que se actualiza con regularidad y se administra por separado.

Las implicaciones de esta división son importantes. En la primera visita, puedes renderizar el contenido en el servidor y, luego, instalar el service worker en el cliente. En visitas posteriores, solo debes solicitar los datos.

¿Qué ocurre con la mejora progresiva?

Si bien el service worker no es compatible actualmente con todos los navegadores, la arquitectura de shell del contenido de la aplicación utiliza la mejora progresiva para garantizar que todos puedan acceder al contenido. Por ejemplo, tomemos nuestro proyecto de muestra.

A continuación, puedes ver la versión completa procesada en Chrome, Firefox Nightly y Safari. A la izquierda, puedes ver la versión de Safari, donde el contenido se procesa en el servidor sin un service worker. A la derecha, vemos las versiones nocturnas de Chrome y Firefox con la tecnología del service worker.

Imagen de la shell de la aplicación cargada en Safari, Chrome y Firefox

¿Cuándo tiene sentido usar esta arquitectura?

La arquitectura de shell de aplicación es ideal para las apps y los sitios que son dinámicos. Si tu sitio es pequeño y estático, es probable que no necesites un shell de aplicación y puedas almacenar en caché todo el sitio en un paso oninstall de service worker. Usa el enfoque que tenga más sentido para tu proyecto. Varios frameworks de JavaScript ya fomentan la división de la lógica de tu aplicación del contenido, lo que hace que este patrón sea más sencillo de aplicar.

¿Hay alguna app de producción que ya use este patrón?

La arquitectura de shell de aplicación es posible con solo unos pocos cambios en la IU general de la aplicación y funciona bien para sitios de gran escala, como la app web progresiva de I/O 2015 y la bandeja de entrada de Google.

Imagen de la carga de la carpeta Recibidos de Google. Ilustra Recibidos con un service worker.

Los shells de aplicaciones sin conexión son una gran ventaja en el rendimiento y también se demuestran en la app de Wikipedia sin conexión de Jake Archibald y en la app web progresiva de Flipkart Lite.

Capturas de pantalla de la demostración de Wikipedia de Jake Archibald.

Explicación de la arquitectura

Durante la primera experiencia de carga, tu objetivo es obtener contenido significativo en la pantalla del usuario lo más rápido posible.

Primera carga y carga de otras páginas

Diagrama de la primera carga con el shell de app

En general, la arquitectura de shell de la aplicación hará lo siguiente:

  • Prioriza la carga inicial, pero permite que el service worker almacene en caché el shell de la aplicación para que las visitas repetidas no requieran que el shell se vuelva a recuperar de la red.

  • La carga diferida o la carga en segundo plano de todo lo demás Una buena opción es usar el almacenamiento en caché de lectura para el contenido dinámico.

  • Usa herramientas de service worker, como sw-precache, por ejemplo, para almacenar en caché y actualizar de manera confiable el service worker que administra tu contenido estático. (Más adelante, obtendrás más información sobre sw-precache).

Para lograrlo, sigue estos pasos:

  • Server enviará contenido HTML que el cliente puede procesar y usará encabezados de vencimiento de caché HTTP a futuro para tener en cuenta los navegadores que no admiten el service worker. Entregará nombres de archivo con hashes para habilitar el “control de versiones” y actualizaciones sencillas para más adelante en el ciclo de vida de la aplicación.

  • Las páginas incluirán estilos de CSS intercalados en una etiqueta <style> dentro del <head> del documento para proporcionar un primer procesamiento de imagen rápido de la shell de la aplicación. Cada página cargará de forma asíncrona el JavaScript necesario para la vista actual. Debido a que CSS no se puede cargar de forma asíncrona, podemos solicitar estilos con JavaScript, ya que ES asíncrono, en lugar de controlado por analizadores y síncrono. También podemos aprovechar requestAnimationFrame() para evitar casos en los que podríamos recibir un acierto de caché rápido y hacer que los estilos se conviertan accidentalmente en parte de la ruta de acceso de renderización crítica. requestAnimationFrame() fuerza la pintura del primer fotograma antes de que se carguen los diseños. Otra opción es usar proyectos como loadCSS de Filament Group para solicitar CSS de forma asíncrona mediante JavaScript.

  • El service worker almacenará una entrada almacenada en caché del shell de la aplicación para que, en visitas repetidas, el shell se pueda cargar por completo desde la caché del service worker, a menos que haya una actualización disponible en la red.

Shell de app para el contenido

Una implementación práctica

Hemos escrito un ejemplo totalmente funcional utilizando la arquitectura de shell de la aplicación, JavaScript ES2015 normal para el cliente y Express.js para el servidor. Por supuesto, nada te impide usar tu propia pila para el cliente o las partes del servidor (p. ej., PHP, Ruby, Python).

Ciclo de vida de un service worker

Para nuestro proyecto de shell de aplicación, usamos sw-precache, que ofrece el siguiente ciclo de vida de service worker:

Evento Acción
Instalar Almacena en caché el shell de la aplicación y otros recursos de la app de una sola página.
Activación Borra las cachés antiguas.
Fetch Publica una app web de una sola página para las URLs y usa la caché para los recursos y las partes predefinidas. Usa la red para otras solicitudes.

Bits de servidor

En esta arquitectura, un componente del servidor (en nuestro caso, escrito en Express) debe poder tratar el contenido y la presentación por separado. El contenido podría agregarse a un diseño HTML que dé como resultado una renderización estática de la página, o podría entregarse por separado y cargarse de forma dinámica.

Es comprensible que tu configuración del servidor sea drásticamente diferente de la que usamos para nuestra app de demostración. Este patrón de apps web se puede lograr en la mayoría de las configuraciones del servidor, aunque requiere algunos rediseños. Descubrimos que el siguiente modelo funciona bastante bien:

Diagrama de la arquitectura de shell de app
  • Los extremos se definen para tres partes de tu aplicación: la URL orientada al usuario (índice/comodín), el shell de la aplicación (service worker) y tus parciales HTML.

  • Cada extremo tiene un controlador que incorpora un diseño de handlebars que, a su vez, puede incorporar vistas y partes parciales del manubrio. En pocas palabras, los parciales son vistas que son fragmentos de HTML que se copian en la página final. Nota: Los frameworks de JavaScript que realizan una sincronización de datos más avanzada suelen ser mucho más fáciles de transferir a una arquitectura de shell de aplicación. Tienden a usar la vinculación y sincronización de datos en lugar de elementos parciales.

  • Inicialmente, el usuario recibe una página estática con contenido. Esta página registra un service worker, si es compatible, que almacena en caché el shell de la aplicación y todo lo que depende (CSS, JS, etc.).

  • El shell de app actuará como una app web de una sola página, usando JavaScript para XHR en el contenido de una URL específica. Las llamadas XHR se realizan a un extremo /parials* que devuelve el fragmento pequeño de HTML, CSS y JS necesario para mostrar ese contenido. Nota: Hay muchas formas de abordar esto y XHR es solo una de ellas. Algunas aplicaciones intercalarán sus datos (quizás usando JSON) para la renderización inicial y, por lo tanto, no son “estáticas” en el sentido de HTML aplanado.

  • Los navegadores sin compatibilidad con service worker siempre deben tener una experiencia de resguardo. En nuestra demostración, recurrimos a la renderización estática básica del servidor, pero esta es solo una de muchas opciones. El aspecto de service worker te brinda nuevas oportunidades para mejorar el rendimiento de tu app de estilo de aplicación de una sola página usando la shell de aplicación almacenada en caché.

Control de versiones de archivos

Una pregunta que surge es cómo manejar el control de versiones y la actualización de los archivos. Es específico de la aplicación y las opciones son las siguientes:

  • Primero establece la red y, de lo contrario, usa la versión almacenada en caché.

  • Solo de red y falla si no hay conexión.

  • Almacena en caché la versión anterior y actualízala más tarde.

En la shell de la aplicación en sí, se debe adoptar un enfoque en el que se priorice la caché para la configuración de tu service worker. Si no almacenas en caché el shell de la aplicación, significa que no adoptaste correctamente la arquitectura.

Herramientas

Contamos con varias bibliotecas auxiliares de service worker que facilitan el proceso de almacenamiento previo en caché de la shell de tu aplicación o el manejo de patrones comunes de almacenamiento en caché.

Captura de pantalla del sitio de la biblioteca de Service Worker en Web Fundamentals

Usa sw-precache para la shell de tu aplicación

El uso de sw-precache para almacenar en caché el shell de la aplicación debería manejar las inquietudes sobre las revisiones de archivos, las preguntas de instalación o activación y el escenario de recuperación para el shell de app. Suelta sw-precache en el proceso de compilación de tu aplicación y usa comodines configurables para seleccionar tus recursos estáticos. En lugar de crear manualmente la secuencia de comandos del service worker, deja que sw-precache genere una que administre la caché de manera segura y eficiente, con un controlador de recuperación en caché que prioriza la caché.

Las visitas iniciales a tu app activan el almacenamiento previo en caché del conjunto completo de recursos necesarios. Esto es similar a la experiencia de instalar una aplicación nativa desde una tienda de aplicaciones. Cuando los usuarios regresan a tu app, solo se descargan los recursos actualizados. En nuestra demostración, informamos a los usuarios cuando hay una nueva shell disponible con el mensaje "App updates. Actualizar para obtener la nueva versión". Este patrón es una forma sencilla de informar a los usuarios que pueden actualizar para obtener la versión más reciente.

Usa sw-toolbox para el almacenamiento en caché del entorno de ejecución

Usa sw-toolbox para el almacenamiento en caché del tiempo de ejecución con diversas estrategias según el recurso:

  • cacheFirst para las imágenes, junto con una caché dedicada con nombre que tiene una política de vencimiento personalizada de N maxEntries.

  • networkFirst o más rápido para las solicitudes a la API, según la actualización del contenido deseada El más rápido puede estar bien, pero si hay un feed de API específico que se actualiza con frecuencia, usa networkFirst.

Conclusión

Las arquitecturas de shell de aplicación ofrecen varios beneficios, pero solo tienen sentido para algunas clases de aplicaciones. El modelo aún es joven y valdrá la pena evaluar los beneficios del esfuerzo y el rendimiento general de esta arquitectura.

En nuestros experimentos, aprovechamos el uso compartido de plantillas entre el cliente y el servidor para minimizar el trabajo de creación de dos capas de aplicaciones. Esto garantiza que la mejora progresiva siga siendo una característica central.

Si ya estás considerando usar service workers en tu app, echa un vistazo a la arquitectura y evalúa si tiene sentido para tus propios proyectos.

Agradecemos a nuestros revisores: Jeff Posnick, Paul Lewis, Alex Russell, Seth Thompson, Rob Dodson, Taylor Savage y Joe Medley.