Receba respostas imediatas por streaming

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

A assíncrono é igualmente importante, embora menos visível para o desenvolvedor, quando se trata de respostas fornecidas pelo manipulador de eventos de busca de um service worker. 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, potencialmente, usar analisadores otimizados para streaming para exibir o conteúdo progressivamente.

Ao criar seu próprio manipulador de eventos fetch, é comum transmitir ao método respondWith() um Response (ou uma promessa para um Response) recebido via fetch() ou caches.match() e chamá-lo para um dia. A boa notícia é que as Responses criadas por ambos esses métodos já podem ser transmitidas. A má notícia é que Responses construídos "manualmente" não podem ser transmitidos, pelo menos até agora. É aqui que a API Streams entra em cena.

streams?

Um stream é uma fonte de dados que pode ser criada e manipulada incrementalmente e fornece uma interface para ler ou gravar blocos de dados assíncronos, em que apenas um subconjunto pode estar disponível na memória a qualquer momento. Por enquanto, estamos interessados 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 cuja solicitação acionou o evento fetch receberá uma resposta de streaming de volta assim que event.respondWith() for chamado e continuará a ler a partir desse stream, desde que o service worker continue usando enqueue() dados adicionais. 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 percebeu que o exemplo anterior tinha alguns comentários /* your data here */ de marcador de posição e não tinha muitos detalhes reais de implementação. Então, como seria um exemplo do mundo real?

Jake Archibald tem um ótimo exemplo de como usar streams para unir uma resposta HTML de vários snippets HTML armazenados em cache, além de dados "ao vivo" transmitidos por fetch(). Nesse caso, o conteúdo do próprio blog (em inglês).

A vantagem de usar uma resposta de streaming, como explica Jake (link em inglês), é que o navegador pode analisar e renderizar o HTML durante o streaming, incluindo o bit inicial carregado rapidamente no cache, sem precisar esperar a conclusão de toda a busca do conteúdo do blog. Isso aproveita ao máximo os recursos de renderização HTML progressiva do navegador. Outros recursos que também podem ser renderizados progressivamente, como formatos de imagem e vídeo, também podem se beneficiar dessa abordagem.

streams? Ou shells de apps?

As práticas recomendadas atuais sobre o uso de service workers para potencializar seus apps da Web se concentram em um modelo de App Shell + conteúdo dinâmico. Essa abordagem depende do armazenamento em cache agressivo do "shell" do seu aplicativo da Web (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 com uma solicitação do lado do cliente.

Os streams trazem uma alternativa ao modelo do App Shell, em que há uma resposta HTML mais completa transmitida para o navegador quando um usuário navega para uma nova página. A resposta transmitida pode usar recursos armazenados em cache para que ainda possa fornecer a parte inicial do HTML rapidamente, mesmo off-line. No entanto, elas acabam se parecendo mais com corpos de resposta tradicionais renderizados pelo servidor. Por exemplo, se o app da Web usa um sistema de gerenciamento de conteúdo que renderiza o HTML pelo servidor juntando modelos parciais, esse modelo é convertido diretamente em respostas de streaming, com a lógica de modelo replicada no service worker em vez do servidor. Como demonstrado no vídeo a seguir, para esse caso de uso, a vantagem de velocidade oferecida pelo streaming de respostas pode ser impressionante:

Uma vantagem importante de transmitir toda a resposta HTML, explicando por que essa é 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. Partes de HTML que são inseridas em um documento após o carregamento da página (como é comum no modelo do App Shell) 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 que são renderizadas progressivamente ou um shell leve com uma solicitação de conteúdo dinâmico do lado do cliente? A resposta é que isso depende: se você tem uma implementação atual que depende de um CMS e modelos parciais (vantagem: stream), de se espera payloads HTML grandes e únicos que se beneficiem da renderização progressiva (vantagem: stream); do seu app da Web ser melhor modelado como um aplicativo de página única (vantagem: App Shell); e se você precisa atualmente de um modelo compatível com vários navegadores (vantagem: App Shell).

Ainda estamos no início das respostas de streaming fornecidas por service worker e esperamos ver os diferentes modelos amadurecer e principalmente para ver mais ferramentas desenvolvidas para automatizar casos de uso comuns.

Mais detalhes sobre streams

Se você estiver criando seus próprios streams legíveis, simplesmente chamar controller.enqueue() indiscriminadamente pode não ser suficiente ou eficiente. Jake aborda alguns detalhes sobre 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 quiser ainda mais detalhes, a especificação de Streams pode ajudar.

Compatibilidade

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

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

O progresso no suporte à API Streams sem prefixo no Edge, bem como o suporte geral ao service worker, podem ser acompanhados na página de status da plataforma da Microsoft.