Cualquier persona que haya usado servicios en primer plano podría decirte que son asíncronos en todos los niveles. Se basan exclusivamente en interfaces basadas en eventos, como FetchEvent
, y usan promesas para indicar cuándo se completan las operaciones asíncronas.
La asincronicidad es igual de importante, aunque menos visible para el desarrollador, cuando se trata de las respuestas que proporciona el controlador de eventos de recuperación de un trabajador de servicio. Las respuestas de transmisión son el estándar de oro aquí: permiten que la página que realizó la solicitud original comience a trabajar con la respuesta en cuanto esté disponible el primer fragmento de datos y, potencialmente, use analizadores optimizados para la transmisión para mostrar el contenido de forma progresiva.
Cuando escribes tu propio controlador de eventos fetch
, es común pasarle al método respondWith()
un Response
(o una promesa de Response
) que obtienes a través de fetch()
o caches.match()
, y ya está. La buena noticia es que los Response
que crean ambos métodos ya se pueden transmitir. La mala noticia es que los Response
construidos “manualmente” no se pueden transmitir, al menos hasta ahora. Aquí es donde entra en juego la
API de Streams.
¿Transmisiones?
Un flujo es una fuente de datos que se puede
crear y manipular de forma incremental,
y proporciona una interfaz para leer o escribir fragmentos de datos asíncronos,
de los cuales solo un subconjunto puede estar disponible en la memoria en un momento determinado. Por ahora, nos interesan los ReadableStream
, que se pueden usar para construir un objeto Response
que se pasa a 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 página cuya solicitud activó el evento fetch
recibirá una respuesta de transmisión en cuanto se llame a event.respondWith()
y seguirá leyendo de esa transmisión mientras el trabajador de servicio siga enqueue()
ando datos adicionales. La respuesta que fluye del servicio de trabajo a la página es realmente
asíncrona, y tenemos control total sobre el llenado del flujo.
Usos del mundo real
Es probable que hayas notado que el ejemplo anterior tenía algunos comentarios de marcador de posición /* your data here */
y no incluía muchos detalles de la implementación real.
Entonces, ¿cómo se vería un ejemplo del mundo real?
Jake Archibald (no es de extrañar) tiene un gran ejemplo de cómo usar transmisiones para unir una respuesta HTML a partir de varios fragmentos HTML almacenados en caché, junto con datos "en vivo" transmitidos a través de fetch()
; en este caso, contenido para su blog.
La ventaja de usar una respuesta de transmisión, como explica Jake, es que el navegador puede analizar y renderizar el HTML a medida que se transmite, incluido el bit inicial que se carga rápidamente desde la caché, sin tener que esperar a que se complete la recuperación de todo el contenido del blog. Esto aprovecha al máximo las capacidades de renderización HTML progresiva del navegador. Otros recursos que también se pueden renderizar de forma progresiva, como algunos formatos de imagen y video, también se pueden beneficiar de este enfoque.
¿Transmisiones? ¿O shells de apps?
Las prácticas recomendadas existentes sobre el uso de los servicios de trabajo para potenciar tus apps web se centran en un modelo de shell de app + contenido dinámico. Ese enfoque se basa en almacenar en caché de forma agresiva la "cáscara" de tu aplicación web (el HTML, JavaScript y CSS mínimos necesarios para mostrar tu estructura y diseño) y, luego, cargar el contenido dinámico necesario para cada página específica a través de una solicitud del cliente.
Las transmisiones ofrecen una alternativa al modelo de Shell de la app, en el que hay una respuesta HTML más completa que se transmite al navegador cuando un usuario navega a una página nueva. La respuesta transmitida puede usar recursos almacenados en caché, por lo que aún puede proporcionar el fragmento inicial de HTML rápidamente, incluso sin conexión, pero terminan pareciendo más los cuerpos de respuesta tradicionales renderizados por el servidor. Por ejemplo, si tu app web se ejecuta con un sistema de administración de contenido que renderiza HTML en el servidor uniendo plantillas parciales, ese modelo se traduce directamente en el uso de respuestas de transmisión, con la lógica de plantillas replicada en el trabajador de servicio en lugar de tu servidor. Como se muestra en el siguiente video, para ese caso de uso, la ventaja de velocidad que ofrecen las respuestas transmitidas puede ser sorprendente:
Una ventaja importante de transmitir toda la respuesta HTML, que explica por qué es la alternativa más rápida en el video, es que el HTML renderizado durante la solicitud de navegación inicial puede aprovechar al máximo el analizador de HTML de transmisión del navegador. Los fragmentos de HTML que se insertan en un documento después de que se cargó la página (como es común en el modelo de Shell de la app) no pueden aprovechar esta optimización.
Por lo tanto, si estás en las etapas de planificación de la implementación de tu service worker, ¿qué modelo deberías adoptar? ¿Respuestas transmitidas que se renderizan de forma progresiva o un shell ligero junto con una solicitud del cliente para contenido dinámico? La respuesta, como es de esperar, es que depende de si tienes una implementación existente que se basa en un CMS y plantillas parciales (ventaja: transmisión continua), si esperas cargas útiles HTML únicas y grandes que se beneficiarían de la renderización progresiva (ventaja: transmisión continua), si tu app web se modela mejor como una aplicación de una sola página (ventaja: shell de la app) y si necesitas un modelo que actualmente sea compatible con varias versiones estables de navegadores (ventaja: shell de la app).
Aún estamos en los primeros días de las respuestas de transmisión potenciadas por trabajadores del servicio y esperamos ver cómo maduran los diferentes modelos y, en especial, ver más herramientas desarrolladas para automatizar casos de uso comunes.
Explora más a fondo las transmisiones
Si estás construyendo tus propios flujos legibles, es posible que llamar a controller.enqueue()
de forma indiscriminada no sea suficiente ni eficiente. Jake explica en detalle cómo se pueden usar en conjunto los métodos start()
, pull()
y cancel()
para crear un flujo de datos adaptado a tu caso de uso.
Para quienes quieran aún más detalles, la especificación de flujos es lo que necesitan.
Compatibilidad
En Chrome 52, se agregó compatibilidad para construir un objeto Response
dentro de un service worker con un ReadableStream
como fuente.
La implementación del trabajador de servicio de Firefox aún no admite respuestas respaldadas por ReadableStream
, pero hay un error de seguimiento relevante para la compatibilidad con la API de Streams que puedes seguir.
Puedes hacer un seguimiento del progreso de la compatibilidad con la API de Streams sin prefijo en Edge, junto con la compatibilidad general con los servicios en segundo plano, en la página de estado de la plataforma de Microsoft.