Des réponses immédiates en streaming

Quiconque a utilisé des service workers peut vous dire qu'ils sont asynchrones de bout en bout. Elles reposent exclusivement sur des interfaces basées sur des événements, comme FetchEvent, et utilisent des promesses pour signaler la fin des opérations asynchrones.

L'asynchronicité est tout aussi importante, mais moins visible pour le développeur, lorsqu'il s'agit des réponses fournies par le gestionnaire d'événements de récupération d'un service worker. Les réponses en streaming sont l'étalon-or ici: elles permettent à la page ayant effectué la requête d'origine de commencer à travailler avec la réponse dès que le premier bloc de données est disponible, et peuvent utiliser des analyseurs optimisés pour le streaming afin d'afficher progressivement le contenu.

Lorsque vous écrivez votre propre gestionnaire d'événements fetch, il est courant de simplement transmettre à la méthode respondWith() un Response (ou une promesse pour un Response) que vous obtenez via fetch() ou caches.match(), et de vous arrêter là. La bonne nouvelle est que les Response créés par ces deux méthodes sont déjà disponibles en streaming. La mauvaise nouvelle est que les Response conçus "manuellement" ne sont pas diffusables, du moins jusqu'à présent. C'est là qu'intervient l'API Streams.

Flux ?

Un flux est une source de données pouvant être créée et manipulée de manière incrémentielle. Il fournit une interface permettant de lire ou d'écrire des blocs de données asynchrones, dont seul un sous-ensemble peut être disponible en mémoire à un moment donné. Pour l'instant, nous nous intéressons aux ReadableStream, qui peuvent être utilisés pour créer un objet Response transmis à fetchEvent.respondWith():

self.addEventListener('fetch', event => {
    var stream = new ReadableStream({
    start(controller) {
        if (/* there's more data */) {
        controller.enqueue(/* your data here */);
        } else {
        controller.close();
        }
    });
    });

    var response = new Response(stream, {
    headers: {'content-type': /* your content-type here */}
    });

    event.respondWith(response);
});

La page dont la requête a déclenché l'événement fetch recevra une réponse de streaming dès que event.respondWith() sera appelé, et elle continuera de lire à partir de ce flux tant que le service worker continuera de enqueue()er des données supplémentaires. La réponse du service worker à la page est vraiment asynchrone, et nous contrôlons totalement le remplissage du flux.

Cas d'utilisation concrets

Vous avez probablement remarqué que l'exemple précédent comportait des commentaires d'espace réservé /* your data here */ et était peu détaillé sur les détails d'implémentation. À quoi ressemblerait un exemple concret ?

Jake Archibald (sans surprise) propose un excellent exemple d'utilisation de flux pour assembler une réponse HTML à partir de plusieurs extraits HTML mis en cache, ainsi que des données "en direct" diffusées via fetch() (dans ce cas, le contenu de son blog).

L'avantage d'utiliser une réponse en streaming, comme l'explique Jake, est que le navigateur peut analyser et afficher le code HTML au fur et à mesure de son flux, y compris le bit initial qui est chargé rapidement à partir du cache, sans avoir à attendre que l'extraction de l'intégralité du contenu du blog soit terminée. Cela permet de tirer pleinement parti des fonctionnalités de rendu HTML progressif du navigateur. D'autres ressources pouvant également être affichées progressivement, comme certains formats d'images et de vidéos, peuvent également bénéficier de cette approche.

Flux ? Ou des shells d'application ?

Les bonnes pratiques existantes concernant l'utilisation de service workers pour alimenter vos applications Web se concentrent sur un modèle App Shell + contenu dynamique. Cette approche repose sur la mise en cache agressive de la "coquille" de votre application Web (le HTML, le JavaScript et le CSS minimals nécessaires pour afficher votre structure et votre mise en page), puis sur le chargement du contenu dynamique nécessaire pour chaque page spécifique via une requête côté client.

Les flux constituent une alternative au modèle d'App Shell, dans lequel une réponse HTML plus complète est diffusée dans le navigateur lorsqu'un utilisateur accède à une nouvelle page. La réponse en streaming peut utiliser des ressources mises en cache. Elle peut donc toujours fournir rapidement le premier bloc de code HTML, même en mode hors connexion. Toutefois, elle finit par ressembler davantage aux corps de réponse traditionnels générés par le serveur. Par exemple, si votre application Web est alimentée par un système de gestion de contenu qui effectue le rendu HTML sur le serveur en assemblant des modèles partiels, ce modèle se traduit directement par l'utilisation de réponses en streaming, la logique de création de modèles étant répliquée dans le service worker au lieu de votre serveur. Comme le montre la vidéo suivante, pour ce cas d'utilisation, l'avantage de vitesse sans frais par les réponses en streaming peut être saisissant:

L'un des avantages importants du streaming de l'intégralité de la réponse HTML, qui explique pourquoi il s'agit de l'option la plus rapide dans la vidéo, est que le rendu HTML lors de la requête de navigation initiale peut tirer pleinement parti de l'analyseur HTML en streaming du navigateur. Les blocs de code HTML insérés dans un document après le chargement de la page (comme c'est courant dans le modèle d'App Shell) ne peuvent pas profiter de cette optimisation.

Si vous en êtes aux étapes de planification de l'implémentation de votre service worker, quel modèle devez-vous adopter: des réponses en streaming qui sont progressivement affichées ou un shell léger associé à une requête côté client pour du contenu dynamique ? La réponse est, sans surprise, qu'elle dépend de plusieurs facteurs: si vous disposez d'une implémentation existante qui repose sur un CMS et des modèles partiels (avantage: flux) ; si vous prévoyez des charges utiles HTML uniques et volumineuses qui bénéficieraient du rendu progressif (avantage: flux) ; si votre application Web est mieux modélisée en tant qu'application monopage (avantage: shell d'application) ; et si vous avez besoin d'un modèle actuellement compatible avec les versions stables de plusieurs navigateurs (avantage: shell d'application).

Les réponses de streaming basées sur les service workers en sont encore à leurs débuts. Nous avons hâte de voir les différents modèles mûrir et, surtout, de voir davantage d'outils développés pour automatiser les cas d'utilisation courants.

Présentation détaillée des flux

Si vous créez vos propres flux lisibles, appeler simplement controller.enqueue() de manière indiscriminée peut ne pas être suffisant ni efficace. Jake explique en détail comment les méthodes start(), pull() et cancel() peuvent être utilisées en tandem pour créer un flux de données adapté à votre cas d'utilisation.

Pour en savoir plus, consultez la spécification Streams.

Compatibilité

La possibilité de créer un objet Response dans un service worker à l'aide d'un ReadableStream comme source a été ajoutée dans Chrome 52.

L'implémentation du service worker de Firefox n'est pas encore compatible avec les réponses basées sur des ReadableStream, mais vous pouvez suivre un bug de suivi pertinent pour la prise en charge de l'API Streams.

Vous pouvez suivre la progression de la compatibilité avec l'API Streams non préfixée dans Edge, ainsi que la compatibilité globale avec les service workers, sur la page d'état de la plate-forme de Microsoft.