Um caso de uso concreto de worker da Web

No último módulo, apresentamos uma visão geral dos workers da Web. Os Web workers podem melhorar a capacidade de resposta da entrada movendo o JavaScript da linha de execução principal para separar as linhas de execução de worker da Web, o que pode ajudar a melhorar a Interaction to Next Paint (INP) do seu site quando você tem um trabalho que não precisa de acesso direto à linha de execução principal. No entanto, uma visão geral por si só não é suficiente. Neste módulo, vamos apresentar um caso de uso concreto para um web worker.

Um desses casos de uso pode ser um site que precise remover os metadados Exif de uma imagem. Esse não é um conceito tão improvável. Na verdade, sites como o Flickr oferecem aos usuários uma maneira de visualizar metadados Exif para aprender detalhes técnicos sobre as imagens que eles hospedam, como profundidade de cor, marca e modelo da câmera e outros dados.

No entanto, a lógica para buscar uma imagem, convertê-la em um ArrayBuffer e extrair os metadados EXIF pode ser potencialmente cara se feita totalmente na linha de execução principal. Felizmente, o escopo do Web worker permite esse trabalho feito fora da linha de execução principal. Em seguida, usando o pipeline de mensagens do worker da Web, os metadados Exif são transmitidos de volta para a linha de execução principal como uma string HTML e mostrados ao usuário.

Como é a linha de execução principal sem um Web worker

Primeiro, observe a aparência da linha de execução principal quando fazemos esse trabalho sem um web worker. Para fazer isso, siga estas etapas:

  1. Abra uma nova guia no Chrome e abra o DevTools.
  2. Abra o painel de performance.
  3. Acesse https://exif-worker.glitch.me/without-worker.html.
  4. No painel de desempenho, clique em Record no canto superior direito do painel DevTools.
  5. Cole este link de imagem (ou outro link de sua escolha que contenha metadados Exif) no campo e clique no botão Obter esse JPEG!.
  6. Quando a interface for preenchida com metadados Exif, clique em Record novamente para interromper a gravação.
O Performance Profiler mostrando a atividade do app extrator de metadados de imagem ocorrendo inteiramente na linha de execução principal. Há duas tarefas longas e substanciais: uma que executa uma busca para obter a imagem solicitada e decodificá-la, e outra que extrai os metadados da imagem.
Atividade da linha de execução principal no app extrator de metadados de imagem. Observe que todas as atividades ocorrem na linha de execução principal.

Observe que, além de outras linhas de execução que podem estar presentes, como linhas de execução de varredura e assim por diante, tudo no app ocorre na linha de execução principal. Na linha de execução principal, acontece o seguinte:

  1. O formulário recebe a entrada e envia uma solicitação fetch para receber a parte inicial da imagem que contém os metadados Exif.
  2. Os dados da imagem são convertidos em um ArrayBuffer.
  3. O script exif-reader é usado para extrair os metadados EXIF da imagem.
  4. Os metadados são raspados para criar uma string HTML, que preenche o visualizador de metadados.

Agora compare isso com uma implementação do mesmo comportamento, mas usando um web worker.

Aparência da linha de execução principal com um worker da Web.

Agora que você já sabe como extrair os metadados Exif de um arquivo JPEG na linha de execução principal, confira a aparência quando um worker da Web está na combinação:

  1. Abra outra guia no Chrome e abra o DevTools.
  2. Abra o painel de performance.
  3. Acesse https://exif-worker.glitch.me/with-worker.html.
  4. No painel de performance, clique no botão de gravação no canto superior direito do painel do DevTools.
  5. Cole este link da imagem no campo e clique no botão Faça o upload do JPEG!.
  6. Quando a interface for preenchida com metadados Exif, clique no botão record novamente para interromper a gravação.
O Performance Profiler mostrando a atividade do app extrator de metadados de imagem que ocorre na linha de execução principal e na linha de execução de worker da Web. Ainda há tarefas longas na linha de execução principal, mas elas são substancialmente menores. A busca/decodificação da imagem e a extração de metadados ocorrem inteiramente em uma linha de execução de worker da Web. O único trabalho da linha de execução principal envolve a transmissão de dados de e para o worker da Web.
Atividade da linha de execução principal no app extrator de metadados de imagem. Há outra linha de execução de worker da Web em que a maior parte do trabalho é feita.

Esse é o poder de um web worker. Em vez de fazer tudo na linha de execução principal, tudo, exceto o preenchimento do visualizador de metadados com HTML, é feito em uma linha de execução separada. Isso significa que a linha de execução principal é liberada para fazer outro trabalho.

Talvez a maior vantagem seja que, ao contrário da versão desse app que não usa um Web worker, o script exif-reader não é carregado na linha de execução principal, mas sim na linha de execução de worker da Web. Isso significa que o custo de download, análise e compilação do script exif-reader ocorre fora da linha de execução principal.

Agora, vamos analisar o código do worker da Web que torna tudo isso possível.

Análise do código do web worker

Não basta ver a diferença que um worker da Web faz, isso também ajuda entender, pelo menos neste caso, como é esse código para que você saiba o que é possível no escopo do worker da Web.

Comece com o código da linha de execução principal que precisa ocorrer antes que o Web worker possa inserir a imagem:

// scripts.js

// Register the Exif reader web worker:
const exifWorker = new Worker('/js/with-worker/exif-worker.js');

// We have to send image requests through this proxy due to CORS limitations:
const imageFetchPrefix = 'https://res.cloudinary.com/demo/image/fetch/';

// Necessary elements we need to select:
const imageFetchPanel = document.getElementById('image-fetch');
const imageExifDataPanel = document.getElementById('image-exif-data');
const exifDataPanel = document.getElementById('exif-data');
const imageInput = document.getElementById('image-url');

// What to do when the form is submitted.
document.getElementById('image-form').addEventListener('submit', event => {
  // Don't let the form submit by default:
  event.preventDefault();

  // Send the image URL to the web worker on submit:
  exifWorker.postMessage(`${imageFetchPrefix}${imageInput.value}`);
});

// This listens for the Exif metadata to come back from the web worker:
exifWorker.addEventListener('message', ({ data }) => {
  // This populates the Exif metadata viewer:
  exifDataPanel.innerHTML = data.message;
  imageFetchPanel.style.display = 'none';
  imageExifDataPanel.style.display = 'block';
});

Esse código é executado na linha de execução principal e configura o formulário para enviar o URL da imagem ao web worker. A partir daí, o código do worker da Web começa com uma instrução importScripts que carrega o script exif-reader externo e, em seguida, configura o pipeline de mensagens para a linha de execução principal:

// exif-worker.js

// Import the exif-reader script:
importScripts('/js/with-worker/exifreader.js');

// Set up a messaging pipeline to send the Exif data to the `window`:
self.addEventListener('message', ({ data }) => {
  getExifDataFromImage(data).then(status => {
    self.postMessage(status);
  });
});

Esse bit de JavaScript configura o pipeline de mensagens para que, quando o usuário enviar o formulário com um URL para um arquivo JPEG, o URL chegue ao Web worker. A partir daí, o próximo trecho de código extrai os metadados EXIF do arquivo JPEG, cria uma string HTML e envia esse HTML de volta ao window para ser exibido ao usuário:

// Takes a blob to transform the image data into an `ArrayBuffer`:
// NOTE: these promises are simplified for readability, and don't include
// rejections on failures. Check out the complete web worker code:
// https://glitch.com/edit/#!/exif-worker?path=js%2Fwith-worker%2Fexif-worker.js%3A10%3A5
const readBlobAsArrayBuffer = blob => new Promise(resolve => {
  const reader = new FileReader();

  reader.onload = () => {
    resolve(reader.result);
  };

  reader.readAsArrayBuffer(blob);
});

// Takes the Exif metadata and converts it to a markup string to
// display in the Exif metadata viewer in the DOM:
const exifToMarkup = exif => Object.entries(exif).map(([exifNode, exifData]) => {
  return `
    <details>
      <summary>
        <h2>${exifNode}</h2>
      </summary>
      <p>${exifNode === 'base64' ? `<img src="data:image/jpeg;base64,${exifData}">` : typeof exifData.value === 'undefined' ? exifData : exifData.description || exifData.value}</p>
    </details>
  `;
}).join('');

// Fetches a partial image and gets its Exif data
const getExifDataFromImage = imageUrl => new Promise(resolve => {
  fetch(imageUrl, {
    headers: {
      // Use a range request to only download the first 64 KiB of an image.
      // This ensures bandwidth isn't wasted by downloading what may be a huge
      // JPEG file when all that's needed is the metadata.
      'Range': `bytes=0-${2 ** 10 * 64}`
    }
  }).then(response => {
    if (response.ok) {
      return response.clone().blob();
    }
  }).then(responseBlob => {
    readBlobAsArrayBuffer(responseBlob).then(arrayBuffer => {
      const tags = ExifReader.load(arrayBuffer, {
        expanded: true
      });

      resolve({
        status: true,
        message: Object.values(tags).map(tag => exifToMarkup(tag)).join('')
      });
    });
  });
});

A leitura é um pouco difícil, mas esse também é um caso de uso bastante complexo para Web workers. No entanto, os resultados valem o trabalho, e não apenas se limitam a esse caso de uso. Você pode usar Web workers para tudo, como isolar chamadas fetch e processar respostas, processar grandes quantidades de dados sem bloquear a linha de execução principal. E isso é só para iniciantes.

Ao melhorar o desempenho dos aplicativos da Web, pense em qualquer coisa que possa ser feita de maneira razoável em um contexto de worker da Web. Os ganhos podem ser significativos e levar a uma melhor experiência geral do usuário no seu site.