Um caso de uso concreto de worker da Web

No último módulo, apresentamos uma visão geral dos web workers. Os service workers podem melhorar a capacidade de resposta da entrada movendo o JavaScript da linha de execução principal para linhas de execução separadas do service worker, o que pode ajudar a melhorar a Interação com a próxima renderização (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 não é suficiente. Neste módulo, oferecemos um caso de uso concreto para um service worker.

Um caso de uso é um site que precisa remover metadados Exif de uma imagem. Esse não é um conceito tão absurdo. Na verdade, sites como o Flickr oferecem aos usuários uma maneira de ver os metadados Exif para saber detalhes técnicos sobre as imagens hospedadas, 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 inteiramente na linha de execução principal. Felizmente, o escopo do service worker permite que esse trabalho seja feito fora da linha de execução principal. Em seguida, usando o pipeline de mensagens do web worker, os metadados Exif são transmitidos de volta à linha de execução principal como uma string HTML e exibidos ao usuário.

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

Primeiro, observe como a linha de execução principal fica 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://chrome.dev/learn-performance-exif-worker/without-worker.html.
  4. No painel de performance, clique em Gravar no canto superior direito do painel DevTools.
  5. Cole este link de imagem ou outro de sua escolha que contenha metadados Exif no campo e clique no botão Pegue esse JPEG!.
  6. Quando a interface for preenchida com metadados Exif, clique em Gravar novamente para parar a gravação.
O criador de perfil de desempenho mostrando a atividade do app extrator de metadados de imagem ocorrendo inteiramente na linha de execução principal. Há duas tarefas longas substanciais: uma que executa uma busca para receber e decodificar a imagem solicitada e outra que extrai os metadados da imagem.
Atividade da linha de execução principal no app extrator de metadados de imagem. Observe que toda a atividade ocorre na linha de execução principal.

Além de outras linhas de execução que podem estar presentes, como linhas de execução rasterizadoras 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 extraídos para construir uma string HTML, que preenche o visualizador de metadados.

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

Como a linha de execução principal fica com um service worker

Agora que você viu como é extrair os metadados Exif de um arquivo JPEG na linha de execução principal, confira como fica quando um service worker está envolvido:

  1. Abra outra guia no Chrome e abra o DevTools dela.
  2. Abra o painel de performance.
  3. Acesse https://chrome.dev/learn-performance-exif-worker/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 de imagem no campo e clique no botão Pegue esse JPEG!.
  6. Quando a interface for preenchida com metadados Exif, clique no botão de gravação de novo para interromper a gravação.
O criador de perfil de desempenho mostrando a atividade do app extrator de metadados de imagem na linha de execução principal e em uma linha de execução de worker da Web. Embora ainda haja tarefas longas na linha de execução principal, elas são substancialmente mais curtas, com a busca/decodificação de imagens e a extração de metadados ocorrendo totalmente 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 para e do worker da Web.
Atividade da linha de execução principal no app extrator de metadados de imagem. Observe que há uma linha de execução de worker da Web adicional em que a maior parte do trabalho é feita.

Esse é o poder de um service 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 fica livre para fazer outros trabalhos.

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

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

Uma olhada no código do service worker

Não basta ver a diferença que um service worker faz. Também é útil entender, pelo menos neste caso, como é esse código para saber o que é possível no escopo do service worker.

Comece com o código da linha de execução principal que precisa ocorrer antes que o service worker possa entrar em cena:

// 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 worker da Web. A partir daí, o código do service worker começa com uma instrução importScripts que carrega o script exif-reader externo e 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 trecho de JavaScript configura o pipeline de mensagens para que, quando o usuário envia o formulário com um URL para um arquivo JPEG, o URL chegue ao service worker. Em seguida, este 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 mostrado 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://chrome.dev/learn-performance-exif-worker/js/with-worker/exif-worker.js
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('')
      });
    });
  });
});

É um pouco longo, mas esse também é um caso de uso bastante complexo para web workers. No entanto, os resultados valem a pena e não se limitam a esse caso de uso. É possível usar web workers para todos os tipos de coisas, como isolar chamadas fetch e processar respostas, processar grandes quantidades de dados sem bloquear a linha de execução principal. E isso é só o começo.

Ao melhorar a performance dos seus aplicativos da Web, comece pensando em tudo o que pode ser feito razoavelmente em um contexto de service worker. Os ganhos podem ser significativos e levar a uma experiência do usuário melhor no seu site.