Applications Web à chargement instantané avec une architecture de shell d'application

Un shell d'application est le code HTML, CSS et JavaScript minimal qui alimente une interface utilisateur. Le shell d'application doit:

  • se chargent rapidement
  • être mis en cache
  • afficher du contenu de manière dynamique ;

Un shell d'application est le secret des bonnes performances fiables. Considérez le shell de votre application comme le bundle de code que vous publieriez sur une plate-forme de téléchargement d'applications si vous créiez une application native. Il s'agit de la charge nécessaire pour démarrer, mais ce n'est peut-être pas tout. Elle maintient votre UI locale et extrait le contenu de manière dynamique via une API.

Séparation du shell HTML, JS et CSS de l'application et du contenu HTML

Contexte

L'article Progressive Web Apps d'Alex Russell explique comment une application Web peut évoluer progressivement en fonction de l'utilisation et du consentement de l'utilisateur pour offrir une expérience plus proche d'une application native, avec une prise en charge hors connexion, des notifications push et la possibilité d'être ajoutée à l'écran d'accueil. Cela dépend fortement des fonctionnalités et des avantages en termes de performances des service workers et de leurs capacités de mise en cache. Vous pouvez ainsi vous concentrer sur la rapidité, en offrant à vos applications Web le même chargement instantané et les mêmes mises à jour régulières que vous avez l'habitude de voir dans les applications natives.

Pour profiter pleinement de ces fonctionnalités, nous devons adopter une nouvelle approche des sites Web: l'architecture de shell d'application.

Voyons comment structurer votre application à l'aide d'une architecture de shell d'application enrichie par un service worker. Nous allons examiner le rendu côté client et côté serveur, et partager un exemple de bout en bout que vous pouvez essayer dès aujourd'hui.

Pour insister sur ce point, l'exemple ci-dessous montre la première charge d'une application utilisant cette architecture. Notez le message "L'application est prête à être utilisée hors connexion" en bas de l'écran. Si une mise à jour du shell est disponible ultérieurement, nous pouvons demander à l'utilisateur de l'actualiser pour obtenir la nouvelle version.

Image du service worker exécuté dans DevTools pour le shell de l'application

Que sont les service workers ?

Un service worker est un script qui s'exécute en arrière-plan, indépendamment de votre page Web. Il répond aux événements, y compris aux requêtes réseau effectuées à partir des pages qu'il diffuse et aux notifications push de votre serveur. La durée de vie d'un service worker est intentionnellement courte. Il se réveille lorsqu'il reçoit un événement et ne s'exécute que le temps nécessaire pour le traiter.

Les services workers disposent également d'un ensemble limité d'API par rapport à JavaScript dans un contexte de navigation normal. Il s'agit d'une pratique standard pour les nœuds de calcul sur le Web. Un service worker ne peut pas accéder au DOM, mais peut accéder à des éléments tels que l'API Cache et peut envoyer des requêtes réseau à l'aide de l'API Fetch. L'API IndexedDB et postMessage() peuvent également être utilisées pour la persistance des données et la messagerie entre le service worker et les pages qu'il contrôle. Les événements push envoyés depuis votre serveur peuvent appeler l'API Notification pour accroître l'engagement des utilisateurs.

Un service worker peut intercepter les requêtes réseau effectuées à partir d'une page (ce qui déclenche un événement de récupération sur le service worker) et renvoyer une réponse récupérée sur le réseau, ou à partir d'un cache local, ou même construite de manière programmatique. Il s'agit en fait d'un proxy programmable dans le navigateur. L'avantage est que, quel que soit l'origine de la réponse, la page Web considère qu'aucun service worker n'est impliqué.

Pour en savoir plus sur les service workers, consultez la présentation des service workers.

Avantages en matière de performance

Les services workers sont efficaces pour le stockage en cache hors connexion, mais ils offrent également des avantages de performances importants sous la forme d'un chargement instantané pour les visites répétées sur votre site ou votre application Web. Vous pouvez mettre en cache le shell de votre application afin qu'il fonctionne hors connexion et que son contenu soit renseigné à l'aide de JavaScript.

Lors de visites répétées, cela vous permet d'obtenir des pixels pertinents à l'écran sans le réseau, même si votre contenu en provient finalement. Imaginez que vous affichez les barres d'outils et les fiches immédiatement, puis que vous chargez le reste de votre contenu progressivement.

Pour tester cette architecture sur des appareils réels, nous avons exécuté notre exemple de shell d'application sur WebPageTest.org et présenté les résultats ci-dessous.

Test 1:Test sur câble avec un Nexus 5 à l'aide de Chrome Dev

La première vue de l'application doit récupérer toutes les ressources du réseau et ne parvient à effectuer une peinture significative qu'au bout de 1,2 seconde. Grâce au cache du service worker, notre nouvelle visite effectue une peinture significative et le chargement est terminé en 0,5 seconde.

Schéma de test de la page Web pour le branchement des câbles

Test 2:Test sur 3G avec un Nexus 5 avec Chrome Dev

Nous pouvons également tester notre exemple avec une connexion 3G légèrement plus lente. Cette fois, il faut 2,5 secondes pour que le premier "First Meaningful Paint" s'affiche lors de la première visite. La page met 7,1 secondes à se charger complètement. Grâce au cache du service worker, notre visite répétée effectue une peinture significative et le chargement est terminé en 0,8 seconde.

Schéma de peinture de test de page Web pour une connexion 3G

D'autres points de vue racontent une histoire similaire. Comparez les trois secondes nécessaires pour obtenir la première peinture significative dans le shell de l'application:

Chronologie de la peinture pour la première vue à partir du test de la page Web

contre 0,9 seconde lorsque la même page est chargée à partir du cache de notre service worker. Cela permet de gagner plus de deux secondes pour nos utilisateurs finaux.

Chronologie de la peinture pour la vue répétée à partir du test de la page Web

Vous pouvez obtenir des performances similaires et fiables pour vos propres applications à l'aide de l'architecture de shell d'application.

Le service worker nous oblige-t-il à repenser la structure des applications ?

Les services workers impliquent des modifications subtiles de l'architecture de l'application. Plutôt que de compresser toute votre application dans une chaîne HTML, il peut être utile de procéder à des opérations AJAX. Vous disposez alors d'un shell (qui est toujours mis en cache et peut toujours démarrer sans le réseau) et de contenus mis à jour régulièrement et gérés séparément.

Les conséquences de cette scission sont importantes. Lors de la première visite, vous pouvez afficher le contenu sur le serveur et installer le service worker sur le client. Lors des visites suivantes, vous n'avez qu'à demander des données.

Qu'en est-il de l'amélioration progressive ?

Bien que les navigateurs ne soient pas tous compatibles avec les service workers pour le moment, l'architecture de la coque de contenu de l'application utilise l'amélioration progressive pour que tous les utilisateurs puissent accéder au contenu. Prenons l'exemple de notre projet.

Vous pouvez voir ci-dessous la version complète affichée dans Chrome, Firefox Nightly et Safari. Tout à gauche, vous pouvez voir la version Safari dans laquelle le contenu est rendu sur le serveur sans service worker. Sur la droite, vous pouvez voir les versions Chrome et Firefox Nightly, qui sont optimisées par un service worker.

Image du shell d'application chargé dans Safari, Chrome et Firefox

Quand utiliser cette architecture ?

L'architecture du shell d'application est la plus adaptée aux applications et sites dynamiques. Si votre site est petit et statique, vous n'avez probablement pas besoin d'un shell d'application et pouvez simplement mettre en cache l'ensemble du site dans une étape oninstall de service worker. Utilisez l'approche la plus adaptée à votre projet. Un certain nombre de frameworks JavaScript encouragent déjà à séparer la logique de l'application du contenu, ce qui facilite l'application de ce modèle.

Des applications de production utilisent-elles déjà ce modèle ?

L'architecture de shell d'application est possible avec quelques modifications apportées à l'interface utilisateur globale de votre application. Elle a bien fonctionné pour les sites à grande échelle tels que l'application Web progressive I/O 2015 de Google et Inbox de Google.

Image de la boîte de réception Google en cours de chargement. Illustration de la boîte de réception à l'aide d'un service worker.

Les shells d'application hors connexion sont un avantage majeur en termes de performances. Ils sont également bien illustrés dans l'application Wikipedia hors connexion de Jake Archibald et dans l'application Web progressive de Flipkart Lite.

Capture d'écran de la démonstration Wikipedia de Jake Archibald.

Expliquer l'architecture

Lors du premier chargement, votre objectif est de diffuser un contenu pertinent sur l'écran de l'utilisateur aussi rapidement que possible.

Premier chargement et chargement d'autres pages

Diagramme de la première charge avec le shell d'application

En général, l'architecture du shell d'application:

  • Priorisez le chargement initial, mais laissez le service worker mettre en cache le shell de l'application afin que les visites répétées ne nécessitent pas de le récupérer à nouveau sur le réseau.

  • Chargez tout le reste en mode lazy-load ou en arrière-plan. Une bonne option consiste à utiliser le mise en cache en lecture pour le contenu dynamique.

  • Utilisez des outils de service worker, tels que sw-precache, par exemple pour mettre en cache et mettre à jour de manière fiable le service worker qui gère votre contenu statique. (Nous reviendrons sur sw-precache plus tard.)

Pour ce faire:

  • Le serveur envoie du contenu HTML que le client peut afficher et utilise des en-têtes d'expiration du cache HTTP à très long terme pour tenir compte des navigateurs qui ne sont pas compatibles avec le service worker. Il fournit des noms de fichiers à l'aide de hachages pour permettre à la fois la gestion des versions et les mises à jour faciles ultérieurement dans le cycle de vie de l'application.

  • Page(s) inclura des styles CSS intégrés dans une balise <style> dans le document <head> pour fournir une première peinture rapide de l'enveloppe de l'application. Chaque page charge de manière asynchrone le code JavaScript nécessaire à la vue actuelle. Étant donné que le CSS ne peut pas être chargé de manière asynchrone, nous pouvons demander des styles à l'aide de JavaScript, car il est asynchrone plutôt que synchrone et contrôlé par l'analyseur. Nous pouvons également tirer parti de requestAnimationFrame() pour éviter les cas où nous pourrions obtenir un cache rapide et que les styles deviendraient accidentellement partie intégrante du chemin de rendu critique. requestAnimationFrame() force le premier frame à être peint avant le chargement des styles. Vous pouvez également utiliser des projets tels que loadCSS de Filament Group pour demander des fichiers CSS de manière asynchrone à l'aide de JavaScript.

  • Le service worker stocke une entrée mise en cache de l'enveloppe de l'application afin que, lors des visites répétées, l'enveloppe puisse être entièrement chargée à partir du cache du service worker, sauf si une mise à jour est disponible sur le réseau.

Shell d&#39;application pour le contenu

Implémentation pratique

Nous avons écrit un exemple entièrement fonctionnel à l'aide de l'architecture de shell d'application, du JavaScript ES2015 standard pour le client et d'Express.js pour le serveur. Bien entendu, rien ne vous empêche d'utiliser votre propre pile pour les parties client ou serveur (par exemple, PHP, Ruby ou Python).

Cycle de vie des service workers

Pour notre projet de shell d'application, nous utilisons sw-precache, qui propose le cycle de vie de service worker suivant:

Événement Action
Installer Mettez en cache le shell de l'application et d'autres ressources d'application monopage.
Activer Videz les anciens caches.
Récupérer Proposez une application Web à une seule page pour les URL et utilisez le cache pour les composants et les parties prédéfinies. Utilisez le réseau pour les autres requêtes.

Bits de serveur

Dans cette architecture, un composant côté serveur (dans notre cas, écrit en Express) doit pouvoir traiter le contenu et la présentation séparément. Vous pouvez ajouter du contenu à une mise en page HTML pour obtenir un rendu statique de la page, ou le diffuser séparément et le charger dynamiquement.

Il est compréhensible que votre configuration côté serveur puisse différer considérablement de celle que nous utilisons pour notre application de démonstration. Ce modèle d'application Web est réalisable avec la plupart des configurations de serveur, mais nécessite une certaine refonte. Nous avons constaté que le modèle suivant fonctionne très bien:

Schéma de l&#39;architecture du shell d&#39;application
  • Les points de terminaison sont définis pour trois parties de votre application: les URL destinées à l'utilisateur (index/wildcard), le shell de l'application (service worker) et vos partiels HTML.

  • Chaque point de terminaison dispose d'un contrôleur qui extrait une mise en page de barres de contrôle, qui peut à son tour extraire des vues et des parties de barres de contrôle. En termes simples, les vues partielles sont des morceaux de code HTML qui sont copiés dans la page finale. Remarque: Les frameworks JavaScript qui effectuent une synchronisation de données plus avancée sont souvent beaucoup plus faciles à porter vers une architecture de shell d'application. Ils ont tendance à utiliser la liaison de données et la synchronisation plutôt que les partiels.

  • L'utilisateur reçoit initialement une page statique contenant du contenu. Cette page enregistre un service worker, si celui-ci est compatible, qui met en cache le shell de l'application et tout ce dont il dépend (CSS, JS, etc.).

  • Le shell d'application agit alors comme une application Web à page unique, utilisant JavaScript pour XHR dans le contenu d'une URL spécifique. Les appels XHR sont effectués à un point de terminaison /partials* qui renvoie le petit extrait de code HTML, CSS et JS nécessaire pour afficher ce contenu. Remarque: Il existe de nombreuses façons d'y parvenir, et XHR n'en est qu'une. Certaines applications insèrent leurs données en ligne (peut-être à l'aide de JSON) pour l'affichage initial et ne sont donc pas "statiques" au sens HTML tronqué.

  • Les navigateurs sans prise en charge des service workers doivent toujours bénéficier d'une expérience de remplacement. Dans notre démonstration, nous utilisons le rendu statique côté serveur de base, mais il ne s'agit que d'une des nombreuses options. L'aspect service worker vous offre de nouvelles possibilités d'améliorer les performances de votre application de style application monopage à l'aide du shell d'application mis en cache.

Gestion des versions de fichiers

Une question se pose : comment gérer le contrôle des versions et la mise à jour des fichiers ? Il s'agit d'une option spécifique à l'application. Les options sont les suivantes:

  • Utilisez d'abord le réseau, puis la version mise en cache.

  • Réseau uniquement et échec en cas de connexion hors connexion.

  • Mettre en cache l'ancienne version et la mettre à jour ultérieurement

Pour le shell de l'application lui-même, vous devez adopter une approche de mise en cache prioritaire pour la configuration de votre service worker. Si vous ne mettez pas en cache le shell de l'application, vous n'avez pas correctement adopté l'architecture.

Outils

Nous gérons un certain nombre de bibliothèques d'assistance de service worker qui facilitent la configuration du précaching de l'enveloppe de votre application ou de la gestion des modèles de mise en cache courants.

Capture d&#39;écran du site de la bibliothèque de service workers sur les principes fondamentaux du Web

Utiliser sw-precache pour votre shell d'application

L'utilisation de sw-precache pour mettre en cache le shell de l'application devrait résoudre les problèmes liés aux révisions de fichiers, aux questions d'installation/activation et au scénario de récupération du shell de l'application. Ajoutez sw-precache au processus de compilation de votre application et utilisez des caractères génériques configurables pour récupérer vos ressources statiques. Plutôt que de créer manuellement votre script de service worker, laissez sw-precache en générer un qui gère votre cache de manière sécurisée et efficace, à l'aide d'un gestionnaire de récupération en cache-first.

Les premières visites de votre application déclenchent le préchargement de l'ensemble complet des ressources nécessaires. Cela ressemble à l'expérience d'installation d'une application native à partir d'une plate-forme de téléchargement. Lorsque les utilisateurs reviennent dans votre application, seules les ressources mises à jour sont téléchargées. Dans notre démonstration, nous informons les utilisateurs lorsqu'un nouveau shell est disponible avec le message "App updates. Actualisez la page pour afficher la nouvelle version." Ce modèle permet aux utilisateurs de savoir qu'ils peuvent actualiser la page pour obtenir la dernière version.

Utiliser sw-toolbox pour la mise en cache d'exécution

Utilisez sw-toolbox pour la mise en cache d'exécution avec des stratégies variables en fonction de la ressource:

  • cacheFirst pour les images, avec un cache nommé dédié qui dispose d'une stratégie d'expiration personnalisée de N maxEntries.

  • networkFirst ou la plus rapide pour les requêtes API, en fonction de la fraîcheur du contenu souhaitée. La valeur "fastest" peut convenir, mais si un flux d'API spécifique est mis à jour fréquemment, utilisez "networkFirst".

Conclusion

Les architectures de shell d'application présentent plusieurs avantages, mais ne sont adaptées qu'à certaines classes d'applications. Le modèle est encore jeune, et il sera intéressant d'évaluer l'effort et les avantages en termes de performances globaux de cette architecture.

Dans nos tests, nous avons profité du partage de modèles entre le client et le serveur pour réduire le travail de création de deux couches d'application. Cela garantit que l'amélioration progressive reste une fonctionnalité de base.

Si vous envisagez déjà d'utiliser des service workers dans votre application, examinez l'architecture et évaluez si elle est adaptée à vos propres projets.

Merci à nos relecteurs: Jeff Posnick, Paul Lewis, Alex Russell, Seth Thompson, Rob Dodson, Taylor Savage et Joe Medley.