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

Addy Osmani
Addy Osmani

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

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

Un 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 crearas una aplicación nativa. Es la carga necesaria para despegar, pero es posible que no sea todo el panorama. Mantiene tu IU local y extrae contenido de forma dinámica a través de una API.

Separación de shell de app de shell de HTML, JS y CSS y el contenido HTML

Información general

En el artículo Apps web progresivas de Alex Russell, se describe cómo una app web puede cambiar progresivamente mediante el uso y el consentimiento del usuario para proporcionar una experiencia más similar a la de una app nativa, completa con compatibilidad 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, podrás enfocarte en la velocidad, ya que brinda a tus aplicaciones web la misma carga instantánea y actualizaciones regulares que sueles ver en las aplicaciones nativas.

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

Veamos cómo estructurar tu app con una arquitectura de shell de aplicación aumentada de service worker. Analizaremos el procesamiento del cliente y del servidor, y compartiremos una muestra de extremo a extremo que puedes probar hoy.

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

Imagen de un service worker que se ejecuta en Herramientas para desarrolladores para la shell de la aplicación

¿Qué son los service workers?

Un service worker es una secuencia de comandos que se ejecuta en segundo plano, separada de tu página web. Responde a eventos, incluidas las solicitudes de red realizadas desde las páginas en las que publica y envía notificaciones 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.

Además, los service workers tienen un conjunto limitado de APIs en comparación con JavaScript en un contexto de navegación normal. Esto es estándar para los trabajadores en la Web. Un service worker no puede acceder al DOM, pero puede acceder a elementos como la API de caché y puede realizar solicitudes de red con la API de recuperación. La API de IndexedDB y postMessage() también están disponibles para usarse con la persistencia de datos y la mensajería entre el service worker y las páginas que controla. Los eventos push 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 cual activa un evento fetch 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. Lo bueno es que, independientemente de dónde provenga la respuesta, parece que la página web parece que no estaban involucrados los service worker.

Para obtener más información sobre los service workers en detalle, lee una Introducción a Service Workers.

Beneficios en el rendimiento

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

En las visitas repetidas, se obtienen píxeles significativos en la pantalla sin la red, incluso si el contenido finalmente proviene de allí. Es como mostrar barras de herramientas y tarjetas inmediatamente y, luego, cargar el resto del contenido de manera 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 el cable con un Nexus 5 mediante Chrome Dev

La primera vista de la app debe recuperar todos los recursos de la red y no logra un procesamiento de imagen significativo hasta los 1.2 segundos. Gracias al almacenamiento en caché de los service workers, nuestra visita repetida logra un procesamiento de imagen significativo y termina de cargarse por completo en 0.5 segundos.

Diagrama de pintura de prueba de una página web para la conexión por cable

Prueba 2: Pruebas en 3G con un Nexus 5 mediante Chrome Dev

También podemos probar nuestra muestra con una conexión 3G un poco más lenta. Esta vez, para nuestro primer procesamiento de imagen con significado, tardará 2.5 segundos en la primera visita. La página tarda 7.1 segundos en cargarse por completo. Con el almacenamiento en caché de los service workers, nuestra visita repetida logra un procesamiento de imagen significativo 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 lleva obtener la primera pintura significativa en la shell de la aplicación:

Pintar el cronograma de la primera vista desde la prueba de página web

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

Pintar el cronograma para la vista repetida desde la prueba de página web

Con la arquitectura de shell de aplicación, tus propias aplicaciones pueden obtener mejoras similares y confiables.

¿El service worker requiere que reconsideremos la estructura de las apps?

Los service workers implican algunos cambios sutiles en la arquitectura de la aplicación. En lugar de comprimir toda tu aplicación en una string HTML, puede resultar beneficioso hacer cosas al estilo AJAX. Aquí es donde tienes una 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 procesar contenido en el servidor e instalar el service worker en el cliente. En visitas posteriores, solo necesita solicitar datos.

¿Qué sucede con la mejora progresiva?

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

A continuación, puedes ver la versión completa renderizada en Chrome, Firefox Nightly y Safari. A la izquierda, puede ver la versión de Safari en la que el contenido se procesa en el servidor sin un service worker. A la derecha, se muestran las versiones Nightly de Chrome y Firefox, que cuentan con la tecnología de service worker.

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

¿Cuándo tiene sentido usar esta arquitectura?

La arquitectura de shell de aplicación tiene más sentido para las aplicaciones y los sitios que son dinámicos. Si tu sitio es pequeño y estático, es probable que no necesites una shell de aplicación y puedas simplemente almacenar en caché todo el sitio en un paso oninstall del 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.

¿Ya hay apps de producción que usen este patrón?

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

Imagen en la que se está cargando la bandeja de entrada de Google. Ilustración de Recibidos con un service worker.

Las shells de aplicaciones sin conexión representan una gran victoria de rendimiento y también se demuestran bien en la app de Wikipedia sin conexión de Jake Archibald y en la aplicación web progresiva de Flipkart Lite.

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

Explicar la arquitectura

Durante la experiencia de la primera carga, tu objetivo es llevar contenido significativo a 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é la shell de la aplicación para que las visitas repetidas no requieran que el shell se vuelva a obtener de la red.

  • Carga diferida o carga en segundo plano 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. (Obtén más información sobre sw-precache más adelante).

Para lograrlo, haz lo siguiente:

  • El servidor enviará contenido HTML que el cliente puede procesar y usará encabezados de vencimiento de caché HTTP en el futuro para compensar los navegadores sin compatibilidad con service worker. Entregará nombres de archivo que usen hashes a fin de habilitar el “control de versiones” y las actualizaciones sencillas para más adelante en el ciclo de vida de la aplicación.

  • Las páginas incluirán estilos CSS intercalados en una etiqueta <style> dentro del documento <head> para proporcionar un primer procesamiento de imagen rápido de la shell de la aplicación. Cada página cargará de manera asíncrona el código 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 es síncrono. También podemos aprovechar requestAnimationFrame() para evitar casos en los que podamos obtener un acierto de caché rápido y terminar con estilos que se conviertan accidentalmente en parte de la ruta de renderización crítica. requestAnimationFrame() obliga a que se pinte el 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 con JavaScript.

  • El service worker almacenará una entrada almacenada en caché del shell de la app de modo que, en visitas repetidas, se pueda cargar por completo el shell 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

Creamos una muestra completamente funcional con la arquitectura de shell de la aplicación, JavaScript estándar ES2015 para el cliente y Express.js para el servidor. Por supuesto, nada te impedirá usar tu propia pila para las partes del cliente o del servidor (p. ej., PHP, Ruby o Python).

Ciclo de vida del 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.
Activar Borra las cachés antiguas.
Recuperar Publica una app web de una sola página para las URLs y usa la caché para los recursos y los parciales predefinidos. Usa la red para otras solicitudes.

Bits de servidor

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

Es comprensible que la configuración del servidor difiera drásticamente de la que usamos para nuestra app de demostración. La mayoría de las configuraciones de servidores pueden lograr este patrón de aplicaciones web, aunque requiere una nueva arquitectura. Descubrimos que el siguiente modelo funciona bastante bien:

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

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

  • Inicialmente, se muestra al usuario una página estática con contenido. En esta página, se registra un service worker, si es compatible, que almacena en caché el shell de la app y todos los elementos de los que depende (CSS, JS, etc.).

  • La shell de la app actuará como una aplicación web de una sola página y usará JavaScript para XHR en el contenido de una URL específica. Las llamadas XHR se realizan a un extremo /partials* que muestra el pequeño fragmento de HTML, CSS y JS que se necesita para mostrar ese contenido. Nota: Existen muchas formas de abordar esto, y los XHR son solo una de ellas. Algunas aplicaciones intercalarán sus datos (tal vez con JSON) para la representación inicial y, por lo tanto, no son "estáticas" en el sentido de HTML plano.

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

Control de versiones de archivos

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

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

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

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

En el caso del shell de la aplicación en sí, debes 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, no adoptaste correctamente la arquitectura.

Herramientas

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

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 controlar los problemas relacionados con las revisiones de archivos, las preguntas de instalación o activación y la situación de recuperación del shell de la app. Usa sw-precache en el proceso de compilación de la aplicación y usa comodines configurables para recoger los recursos estáticos. En lugar de crear manualmente la secuencia de comandos del service worker, permite que sw-precache genere una que administre tu caché de forma segura y eficiente, mediante un controlador de recuperación 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, les informamos a los usuarios cuando hay un nuevo shell disponible con el mensaje "Actualizaciones de apps. Actualizar para obtener la nueva versión". Este patrón es una forma sencilla de informarles a los usuarios que pueden actualizar para obtener la versión más reciente.

Usa sw-toolbox para almacenar en caché el tiempo de ejecución

Usa sw-toolbox para almacenar en caché el entorno de ejecución con diferentes estrategias según el recurso:

  • cacheFirst para 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. La opción más rápida podría 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 tienen varios beneficios, pero solo tienen sentido para algunas clases de aplicaciones. El modelo aún es nuevo y valdrá la pena evaluar el esfuerzo y los beneficios de 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 crear dos capas de aplicación. Esto garantiza que la mejora progresiva siga siendo una función central.

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

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