Web Worker, a API mais subutilizada da web

A minha opinião pessoal é de que Web Workers deveriam ser muito mais utilizados do que são. Nós perdemos muito tempo falando sobre React, Angular e Vue - se você está lendo isso em 2030, por favor tente não rir, a gente realmente achava que esses frameworks eram uma boa ideia - mas não focamos na base: JavaScript, HTML, CSS e toda a plataforma Web. E Web Workers, novamente na minha opinião, deveriam ser, no mínimo, mais divulgados.

Mas se você entrou nesse post é possível que não faça ideia do que eles são. E tudo bem. O objetivo aqui é:

  1. Descobrir o que eles são
  2. Entender a importância deles
  3. Tirar a sua mente do trabalho por 5 minutos 🤷🏻‍♂️

A sequência de fibonacci

Um dos algoritmos mais famosos de todos os tempos, a sequência de fibonacci pode ser definida como:

1, 1, 2, 3, 5, 8, 13, 21, ...

De outra forma, o índice atual será a soma dos dois números anteriores. Repare no 8. Antes dele vem o 5, e antes desse o 3. 5 + 3 = 8. Pronto.

Olhando por um jeito mais acadêmico, temos que:

Fib(N) = Fib(N-1) + Fib(N-2)

E em JavaScript, essa seria uma implementação válida:

function fib(num) {
  if (num <= 1) return 1;
  return fib(num - 1) + fib(num - 2);
}

Esse algoritmo tem uma complexidade, na notação Big O, de O(2^n). Ou seja, pra cada incremento pequeno no valor de N o algoritmo aumenta exponencialmente o tempo de execução.

Se ficou confuso, o quadro abaixo deixará mais claro:

fibonacci(10); // 0ms
fibonacci(20); // 3ms
fibonacci(30); // 326ms
fibonacci(40); // 39652ms

O tempo de execução cresce muito rápido - exponencialmente, pra ser mais acadêmico - para valores pequenos de N.

Threads

Apesar de a web ter evoluído tremendamente desde o seu início, uma coisa se manteve constante: os browsers disponibilizam uma única thread, por aba aberta, para fazer todo o trabalho necessário que é requerido ao executarmos uma aplicação.

Isso significa que, enquanto a função A não terminar de ser executada, a função B não poderá começar sua execução. Pior ainda: se uma função cara (como a função fibonacci, citada anteriormente) for executada, todo o site estará completamente travado até o final da execução dessa função. Isso é conhecido como DOM Blocking.

Para demonstrar esse problema com mais clareza, criei uma aplicação que tem um contador - que fica incrementando infinitamente - e, acima, temos uma função para o cálculo de fibonacci (que demora alguns segundos pra mostrar o resultado):

Fibonacci sem web worker

Veja o resultado aqui: https://fibonacci-sem-web-worker.surge.sh/

Perceba que a página inteitra travou enquanto o cálculo de fibonacci estava sendo realizado.

Péssima experiência pro usuário.

Mas e se pudéssemos colocar esse cálculo caro numa thread separada? Com isso, o usuário conseguiria navegar normalmente enquanto aguarda pelo resultado. Felizmente, com Web Workers, nós podemos fazer exatamente isso 🎉

Embaixo podemos ver a mesma experiência, mas usando Web Workers:

Fibonacci com web worker

Veja o resultado aqui: https://fibonacci-com-web-worker.surge.sh/

Completamente diferente não? O cálculo retorna exatamente o mesmo resultado, com a mesma performance, mas sem travar a experiência do usuário.

Se eu te convenci do problema, nesse ponto você provavelmente quer saber como é possível fazer algo assim. Então vamos pra parte da implementação.

Aviso rápido: usei React para implementar, mas se você não manja de React não tem problema, garanto que vai conseguir acompanhar.

Como implementar

Web Workers, na prática, funcionam enviando e recebendo eventos. Eles ficam no stand-by, esperando por uma mensagem, e assim que essa mensagem for enviada eles reagem à ela e enviam de volta outra mensagem com o resultado.

Consumindo o Web Worker

Pra ilustrar essa ideia melhor, vamos criar o componente Fibonacci, que vai consumir, e se comunicar, com o Web Worker:

// Fibonacci.jsx
const worker = new Worker("./worker.js");

function Fibonacci() {
  const [fibonacci, setFibonacci] = useState("");

  const update = (e) => {
    setFibonacci(e.data);
  };

  useEffect(() => {
    worker.addEventListener("message", update);

    return () => {
      worker.removeEventListener("message", update);
    };
  }, []);

  /* mais código aqui */
}

Ficou confuso? Vamos quebrar em etapas.

Primeiro, criamos uma instância do Web Worker:

const worker = new Worker("./worker.js");

Depois, usamos essa instância para criarmos um listener. Lembra que eu falei que os Web Workers funcionam com troca de mensagens? Então é basicamente isso:

useEffect(() => {
  worker.addEventListener("message", update);
  return () => {
    worker.removeEventListener("message", update);
  };
}, []);

E pra fechar, atualizamos o estado do componente com o resultado do cálculo - que foi feito no Web Worker:

const update = (e) => {
  setFibonacci(e.data);
};

Tá, mas isso tudo foi sobre consumir o Web Worker. E como faz pra implementá-lo? Por incrível que pareça, essa é a parte mais fácil.

Implemetando o Web Worker

Tá aqui nosso Web Worker:

// worker.js
this.addEventListener("message", (event) => {
  postMessage(fibonacci(event.data));
});

function fibonacci(num) {
  if (num <= 1) return 1;
  return fibonacci(num - 1) + fibonacci(num - 2);
}

Novamente, se ficou confuso, vamos por partes.

Primeiro, o Web Worker precisa escutar eventos:

this.addEventListener("message", (event) => {  postMessage(fibonacci(event.data));
});

Depois, ele realiza algo baseado nesse evento - nesse caso, o cálculo de fibonacci, e depois envia o evento de volta usando o método postMessage:

this.addEventListener("message", (event) => {
  postMessage(fibonacci(event.data));});

E é só isso mesmo 🚀

Mais conteúdo

Se você gostou da ideia dos Web Workers, então recomendo essa apresentação do Surma no Chrome Dev Summit de 2019, onde ele faz um deep dive no assunto:

Nesse post eu mostrei a API nativa dos Web Workers, mas também recomendo que você dê uma olhada nessas duas bibliotecas que facilitam o uso deles no dia-a-dia: Comlink e Workerize