JavaScript: Um guia sobre timers na Web

setTimeout, setInterval, rAF, rIC e como eles funcionam!

Image for post
Image for post
"Time" por Paweł Czerwiński no Unsplash

⭐️ Créditos

Pergunta de entrevista: Qual a diferença entre esses temporizadores em JavaScript:

Sendo mais específico, se você enfileirar todos esses temporizadores de uma vez, você tem alguma idéia de qual ordem eles seriam executados?

Já coçou a cabeça? Acredite, você não está sozinho! Eu venho escrevendo JavaScript e programando para web há anos, já trabalhei para um fornecedor de navegador web por dois anos e foi só recentemente que eu realmente entendi todos esses temporizadores e como eles funcionam juntos.

Nesse artigo, eu irei mostrar em uma visão geral e de alto-nível, como esses temporizadores funcionam e quando você pode usá-los. Também irei falar sobre as funções do Lodash, a debounce() e throttle(), pois eu acredito que elas são bem úteis.

Promises e microtasks

Vamos tirar esse do caminho primeiro pois provavelmente é o mais simples. Um callback de Promise também é chamado de “microtask” e ele roda na mesma frequência que o callback de MutationObserver. Assumindo que a especificação de queueMicrotask passe e chegue aos navegadores, também será a mesma coisa.

Já escrevi bastante sobre Promises. Um rápido equívoco sobre Promises que vale a pena mencionar é que elas não dão uma change de respirar ao navegador. Não é só porque você está enfileirando um callback assíncrono, isso não significa que o navegador pode renderizar, processar o dado imputado ou fazer qualquer coisa que nós desejamos que os navegadores façam.

Por exemplo, vamos dizer que temos uma função que bloqueia a thread principal por 1 segundo:

function block() {
var start = Date.now()
while (Date.now() - start < 1000) { /* ooohhh */ }
}

Se enfileirarmos várias microtasks chamando essa função:

for (var i = 0; i < 100; i++) {
Promise.resolve().then(block)
}

Isso provavelmente irá bloquear o navegador por volta de 100 segundos. Seria a mesma coisa que:

for (var i = 0; i < 100; i++) {
block()
}

Microtasks são executadas imediatamente depois de qualquer execução síncrona seja completada. Não há nenhuma chance de incluir outro tipo de trabalho entre os dois. Se você planejar dividir uma tarefa super longa em pequenas microtasks, provavelmente, elas não serão executadas da maneira que você espera.

setTimeout e setInterval

Esse dois são primos, setTimeout enfileira uma tarefa para ser executada em X milissegundos e setInterval enfileira uma tarefa recorrente para ser executada em X milissegundos.

A verdade é… navegadores não respeitam o tempo especificado. Veja, historicamente, desenvolvedores web abusaram do uso de setTimeout. Esse abuso foi tanto que navegadores tiveram que adicionar mitigações para setTimeout(/* ... */, 0) para evitar o bloqueio da thread principal do navegador. Muitos websites estavam usando setTimeout(0) como se fosse confete.

Essa é a razão plea qual truques em crashmybrowser.com não funcionam mais, tais como enfileirar chamadas setTimeout que chamam mais duas setTimeout e que cada, chamam mais duas setTimeout e assim por diante. Eu escrevi um pouco sobre algumas dessas mitigações no navegador Edge em "Improving input responsiveness in Microsoft Edge".

Em outras palavras, um setTimeout(0) não significa que irá ser executado em zero milissegundos. Geralmente, ele executa em 4. Algumas vezes ele pode executar em 16 (que é o caso do Edge em modo bateria/notebook com apenas a carga da bateria). E outras vezes ele pode ser apertado em 1 segundo (quando executado em uma tab em segundo plano). Esses são os truques que os navegadores tiveram que inventar para impedir que páginas web descontroladas mastiguem sua CPU fazendo inúteis chamadas a setTimeout.

Com isso dito, setTimeout permite que o navegador execute alguma tarefa antes de executar o callback (diferente de microtasks). Mas, se o nosso objetivo é permitir alguma entrada de dados ou renderização aconteça antes de executar o callback, setTimeout não é a melhor opção, pois ele acidentalmente permite que essas coisas aconteçam. Atualmente há melhores APIs nos navegadores que podem se conectar diretamente ao sistema de renderização do navegador.

setImmediate

Antes de falarmos sobre essas “melhores APIs de navegadores”, vale mencionar o setImmediate, que, por falta de significado, ele é... estranho! Se você olhar a definição em caniuse.com, você vai ver que apenas navegadores da Microsoft suportam essa funcionalidade. E para nossa surpresa, ele também existe em Node.js e tem vários "polyfills" no npm. Mas que (*#!@ é essa?

setImmediate foi proposto originalmente pela Microsoft para tentar contornar os problemas de setTimeout descrito acima. Basicamente, setTimeout foi usado de forma abusiva e a idéia foi criar algo novo que permita que setImmediate(0 realmente seja setImmediate(0) e não algo que seja "apertado em 4ms". Você pode ver algumas conversas do Jason Weber sobre isso em 2011.

Infelizmente, setImmediate só foi adotado pelo IE e Edge. A razão pela qual ainda está em uso no IE, é que ele permite que eventos de entrada de dados, como teclado e clicks de mouse, "pulem a fila" e executem antes do callback de setImmediate, pois o IE não tem a mesma mágica para setTimeout (Edge resolveu esse problema, como explicado no link acima).

Um outro fato é que, setImmediate existe em Node.js, significando que vários projetos "node-polyfilled" estão usando isso no navegador sem saber o que isso faz. E não ajuda saber que as diferenças entre a implementação do Node.js de setImmediate e process.nextTick são bem confusas e mesmo na documentação oficial do Node, é explicado que os nomes deveriam ser invertidos (para esse artigo, iremos focar nas soluções em navegadores, Node.js é outro história!).

No final das contas: Use setImmediate se você sabe o que está fazendo e está tentando otimizar entrada de dados no IE. Caso contrário, não se incomode (ou só use isso em Node.js).

requestAnimationFrame

Chegamos na função que irá substituir o setTimeout, sendo um dos mais importantes! Um temporizador que se conecta diretamente ao sistema de renderização do navegador, aliás, se você não sabe como o loop de eventos do navegador funciona, eu recomendo essa apresentação do Jake Archibald.

E requestAnimationFrame basicamente funciona dessa maneira: é como se fosse setTimeout, mas, ao invés de esperar alguma quantidade de tempo imprevisível (4ms, 16ms ou 1ms etc), ele é executado antes da próxima fase de cálculo de style/layout do navegador. Como Jake comentou na apresentação acima, há uma pequena diferença, onde ele realmente executa após esta etapa no Safari, IE e Edge < 18, mas vamos ignorar isso por hora.

A minha perspectiva de requestAnimationFrame é: a qualquer momento, onde for preciso fazer algum trabalho que irá modificar o style/layout do navegador, por exemplo, mudando propriedades CSS ou iniciando animações, eu coloco um requestAnimationFrame (conhecido como rAF), para assegurar algumas coisas:

É por isso que as bibliotecas de animação que não dependem de transições CSS ou keyframes, como GreenShock ou React Motion, tipicamente fazem suas mudanças dentro de um callback rAF. Se você estiver animando um elemento entre opacity: 0 e opacity: 1, não faz sentido em fazer um bilhão de chamadas callback para animar todos os estados intermediários possíveis, incluindo opacity: 0.0000001 e opacity: 0.9999999.

Ao invés disso, é melhor utilizar rAF para deixar o navegador te dizer quantos frames você terá para pintar durante um dado período de tempo, calculando a interpolação para esse frame em particular. Dessa forma, aparelhos mais lentos, naturalmente terminam com um framerate mais lento e aparelhos mais rápidos, com um framerate mais rápido, o que não seria necessariamente verdade se você utilizar algo como setTimeout, que opera independentemente da velocidade de renderização do navegador.

requestIdleCallback

rAF é provavelmente o temporizador mais útil nesse kit de ferramentas, porém, requestIdleCallback vale ser mencionado. O suporte de navegadores não é muito bom, mas existe um polyfill que funciona muito bem (e utiliza rAF por debaixo dos panos).

Em muitos casos, rAF é similar ao rIC. Assim como rAF, o rIC naturalmente se adapta as características de performance do navegador em uso, se o aparelho estiver em uso pesado, rIC pode ser adiado. A diferença é que rIC é executado na fase em que o navegador estiver ocioso, por exemplo, quando o navegador não tem mais nenhuma task, microtask ou entrada de eventos para processar e está livre para fazer algum trabalho. Ele também oferece um "prazo final" para controlar quanto do seu orçamento, em recursos, você está utilizando, o que é um bom sinal.

Dan Abramov tem uma ótima apresentação na JSConf Iceland 2018 onde ele mostra como você pode utilizar rIC. Na apresentação, Dan tem uma aplicação web que chama rIC para cada entrada de dados enquanto o usuário digita em um teclado e então, ele atualiza o estado no callback. Isso é ótimo pois uma digitação muito rápida pode causar vários eventos keydown / keyup muito rápido e não é sempre que precisamos atualizar o estado da página em cada evento da digitação.

Outro bom exemplo disso é um indicador de “contagem de caracteres restantes” no Twitter ou no Mastodon. Eu uso rIC para isso no Pinafore, porque eu realmente não me importo se o indicador é atualizado para cada palavra que eu digito. Se eu estiver digitando rapidamente, é melhor priorizar a capacidade de resposta de entrada para que eu não perca meu senso de fluxo.

Em Pinafore, a pequena barra horizontal e o indicador “caracteres restantes” são atualizados enquanto você digita.
Em Pinafore, a pequena barra horizontal e o indicador “caracteres restantes” são atualizados enquanto você digita.
Em Pinafore, a pequena barra horizontal e o indicador “caracteres restantes” são atualizados enquanto você digita.

No entanto, uma coisa que notei sobre o rIC é que ele é um pouco complicado no Chrome. No Firefox parece ser acionado sempre que eu, intuitivamente, acho que o navegador está "ocioso" e pronto para executar algum código (o mesmo vale para o polyfill). No Chrome para dispositivos móveis, observei que sempre que eu faço um scroll por toque, ele pode atrasar o rIC por vários segundos, mesmo depois de eu terminar de tocar na tela e o navegador não estar fazendo absolutamente nada (eu suspeito que o problema que estou vendo é esse).

ATUALIZAÇÃO: Alex Russell da equipe do Chrome me informou que esse é um problema conhecido e deve ser corrigido em breve!

Em qualquer caso, o rIC é outra ótima ferramenta para adicionar ao seu kit. Eu costumo pensar desta forma: use rAF para trabalho de renderização crítica, use rIC para trabalhos não críticos.

debounce e throttle

Essas duas funções não fazem parte dos navegadores, mas são tão úteis que valem a pena serem mencionadas. Caso você não conheça, há um bom artigo no CSS Tricks.

Meu uso padrão para debounce está dentro de um callback em resize. Quando o usuário redimensiona a janela do navegador, não faz sentido atualizar o layout para cada callback do resize, porque ele é acionado com muita frequência. Ao invés disso, você pode fazer um debounce por alguns milissegundos, o que garantirá que o callback seja acionado assim que o usuário terminar de mexer no tamanho da janela.

o throttle, por outro lado, é algo que eu uso muito mais liberalmente. Por exemplo, um bom caso de uso está dentro de um evento de scroll. Mais uma vez, é inútil tentar atualizar o estado do aplicativo para cada callback do scroll, porque ele é acionado com muita frequência (e a frequência pode variar de navegador para navegador e de método de entrada para método de entrada... argh). Utilizando throttle é possível normalizar esse comportamento e garante que ele seja acionado somente a cada número x de milissegundos. Você também pode ajustar a função throttle (ou debounce) do Lodash para disparar no início do delay, no final, em ambos ou nenhum dos dois.

Por outro lado, eu não usaria o debounce para o cenário do scroll, porque eu não quero que a interface do usuário seja atualizada somente depois que o usuário tiver interrompido explicitamente a rolagem. Isso pode ser irritante ou até mesmo confuso, porque o usuário pode ficar frustrado e tentar manter a rolagem para atualizar o estado da interface do usuário (por exemplo, em uma lista de rolagem infinita). O throttle é melhor neste caso porque ele não espera que o evento de scroll pare de executar para ser executado.

O throttle é uma função que uso em todo o lugar para todos os tipos de entrada do usuário, e até mesmo para algumas tarefas agendadas regularmente, como as limpezas do IndexedDB. É extremamente útil. Talvez um dia, ele possa implementado no navegador!

Conclusão

Esse é o meu tour rápido pelas várias funções temporizadoras disponíveis no navegador e como você pode usá-las!

Provavelmente alguns ficaram de fora, certamente há alguns exóticos por aí (postMessage ou eventos de ciclo de vida, alguém?).

Mas espero que isso, pelo menos, forneça uma boa visão geral de como temporizadoras JavaScript funcionam na web!

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store