Receba respostas imediatas por streaming

Qualquer pessoa que tenha usado service workers pode dizer que eles são assíncronos. Eles dependem exclusivamente de interfaces baseadas em eventos, como FetchEvent, e usam promessas para sinalizar quando as operações assíncronas são concluídas.

A assincronia é igualmente importante, embora menos visível para o desenvolvedor, quando se trata de respostas fornecidas pelo manipulador de eventos de busca de um worker de serviço. As respostas de streaming são o padrão ouro aqui: elas permitem que a página que fez a solicitação original comece a trabalhar com a resposta assim que o primeiro bloco de dados estiver disponível e, possivelmente, use analisadores otimizados para streaming para mostrar o conteúdo de forma progressiva.

Ao escrever seu próprio manipulador de eventos fetch, é comum transmitir ao método respondWith() um Response (ou uma promessa de Response) que você recebe por fetch() ou caches.match(), e pronto. A boa notícia é que as Responses criadas por esses dois métodos já podem ser transmitidas por streaming. A má notícia é que as Responses construídas" manualmente não podem ser transmitidas, pelo menos até agora. É aí que a API Streams entra em cena.

Fluxos?

Um stream é uma origem de dados que pode ser criada e manipulada de forma incremental, e fornece uma interface para ler ou gravar blocos de dados assíncronos, dos quais apenas um subconjunto pode estar disponível na memória a qualquer momento. Por enquanto, nosso interesse está em ReadableStreams, que podem ser usados para construir um objeto Response que é transmitido para 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);
});

A página que acionou o evento fetch vai receber uma resposta de streaming assim que event.respondWith() for chamado e vai continuar lendo esse stream enquanto o worker de serviço continuar enqueue()ndo outros dados. A resposta que flui do service worker para a página é realmente assíncrona, e temos controle total sobre o preenchimento do stream.

Usos reais

Você provavelmente notou que o exemplo anterior tinha alguns comentários de marcador de posição /* your data here */ e não tinha muitos detalhes de implementação. Então, como seria um exemplo real?

Jake Archibald (não é surpresa!) tem um ótimo exemplo de como usar streams para unir uma resposta HTML de vários snippets HTML em cache, junto com dados "ao vivo" transmitidos por fetch(). Neste caso, o conteúdo do blog dele.

A vantagem de usar uma resposta de streaming, como Jake explica, é que o navegador pode analisar e renderizar o HTML conforme ele é transmitido, incluindo o bit inicial que é carregado rapidamente do cache, sem ter que esperar que todo o conteúdo do blog seja buscado. Isso aproveita ao máximo os recursos de renderização progressiva de HTML do navegador. Outros recursos que também podem ser renderizados progressivamente, como alguns formatos de imagem e vídeo, também podem se beneficiar dessa abordagem.

Fluxos? Ou shells de apps?

As práticas recomendadas atuais sobre o uso de service workers para impulsionar seus apps da Web se concentram em um modelo de shell do app + conteúdo dinâmico. Essa abordagem depende do armazenamento em cache agressivo do "shell" do seu aplicativo da Web, ou seja, o HTML, JavaScript e CSS mínimos necessários para mostrar a estrutura e o layout, e, em seguida, carregar o conteúdo dinâmico necessário para cada página específica por meio de uma solicitação do lado do cliente.

As transmissões oferecem uma alternativa ao modelo de shell do app, em que há uma resposta HTML mais completa transmitida para o navegador quando um usuário navega até uma nova página. A resposta transmitida pode usar recursos armazenados em cache, para que possa ainda fornecer o bloco inicial de HTML rapidamente, mesmo off-line. No entanto, ela acaba parecendo mais com corpos de resposta renderizados pelo servidor. Por exemplo, se o app da Web for executado por um sistema de gerenciamento de conteúdo que renderiza HTML no servidor unindo modelos parciais, esse modelo se traduz diretamente no uso de respostas de streaming, com a lógica de modelagem replicada no worker de serviço em vez do servidor. Como o vídeo abaixo demonstra, para esse caso de uso, a vantagem de velocidade que as respostas transmitidas oferecem pode ser impressionante:

Uma vantagem importante do streaming de toda a resposta HTML, explicando por que ela é a alternativa mais rápida no vídeo, é que o HTML renderizado durante a solicitação de navegação inicial pode aproveitar ao máximo o analisador de HTML de streaming do navegador. Os blocos de HTML inseridos em um documento depois que a página é carregada (como é comum no modelo de shell do app) não podem aproveitar essa otimização.

Portanto, se você estiver nos estágios de planejamento da implementação do service worker, qual modelo adotar: respostas transmitidas em streaming renderizadas progressivamente ou um shell leve associado a uma solicitação do lado do cliente para conteúdo dinâmico? A resposta é, não surpreendentemente, que depende: se você tem uma implementação existente que depende de um CMS e modelos parciais (vantagem: stream); se você espera payloads HTML únicos e grandes que se beneficiariam da renderização progressiva (vantagem: stream); se o melhor modelo para seu app da Web é um aplicativo de página única (vantagem: App Shell); e se você precisa de um modelo que seja compatível com várias versões estáveis de vários navegadores (vantagem: App Shell).

Ainda estamos no início das respostas de streaming com base em worker de serviço e esperamos que os diferentes modelos amadureçam e que mais ferramentas sejam desenvolvidas para automatizar casos de uso comuns.

Informações detalhadas sobre transmissões

Se você estiver criando seus próprios fluxos legíveis, chamar controller.enqueue() indiscriminadamente pode não ser suficiente ou eficiente. Jake explica em detalhes como os métodos start(), pull() e cancel() podem ser usados em conjunto para criar um fluxo de dados personalizado para seu caso de uso.

Para quem quer ainda mais detalhes, a especificação de streams tem tudo o que você precisa.

Compatibilidade

Foi adicionado suporte para a construção de um objeto Response dentro de um service worker usando um ReadableStream como fonte no Chrome 52.

A implementação do service worker do Firefox ainda não oferece suporte a respostas com ReadableStreams, mas há um bug de rastreamento relevante para o suporte à API Streams que você pode seguir.

O progresso do suporte à API Streams sem prefixo no Edge, além do suporte ao service worker, pode ser acompanhado na página de status da plataforma da Microsoft.