Você já ouviu "não bloqueie a linha de execução principal" e "divida suas tarefas longas", mas o que isso significa?
Publicado em: 30 de setembro de 2022. Última atualização: 19 de dezembro de 2024
As dicas comuns para manter os apps JavaScript rápidos geralmente se resumem ao seguinte:
- "Não bloqueie a linha de execução principal."
- "Divida suas tarefas longas."
Esse é um ótimo conselho, mas qual trabalho ele envolve? Enviar menos JavaScript é bom, mas isso significa automaticamente interfaces de usuário mais responsivas? Talvez, mas talvez não.
Para entender como otimizar tarefas em JavaScript, primeiro você precisa saber o que são tarefas e como o navegador as processa.
O que é uma tarefa?
Uma tarefa é qualquer parte discreta do trabalho que o navegador realiza. Isso inclui renderização, análise de HTML e CSS, execução de JavaScript e outros tipos de trabalho que você não pode controlar diretamente. De tudo isso, o JavaScript que você escreve é talvez a maior fonte de tarefas.

click
em, mostrada no criador de perfil de desempenho do Chrome DevTools.
As tarefas associadas ao JavaScript afetam o desempenho de algumas maneiras:
- Quando um navegador baixa um arquivo JavaScript durante a inicialização, ele enfileira tarefas para analisar e compilar esse JavaScript para que ele possa ser executado mais tarde.
- Em outros momentos durante a vida útil da página, as tarefas são enfileiradas quando o JavaScript realiza trabalhos como responder a interações por manipuladores de eventos, animações baseadas em JavaScript e atividades em segundo plano, como coleta de dados do Analytics.
Tudo isso, com exceção dos web workers e APIs semelhantes, acontece na linha de execução principal.
O que é a linha de execução principal?
A thread principal é onde a maioria das tarefas é executada no navegador e onde quase todo o JavaScript que você escreve é executado.
A linha de execução principal só pode processar uma tarefa por vez. Qualquer tarefa que leve mais de 50 milissegundos é uma tarefa longa. Para tarefas que excedem 50 milissegundos, o tempo total da tarefa menos 50 milissegundos é conhecido como período de bloqueio da tarefa.
O navegador bloqueia as interações enquanto uma tarefa de qualquer duração está em execução, mas isso não é perceptível para o usuário, desde que as tarefas não sejam executadas por muito tempo. No entanto, quando um usuário tenta interagir com uma página com muitas tarefas longas, a interface parece não responder e pode até ficar quebrada se a linha de execução principal for bloqueada por muito tempo.

Para evitar que a linha de execução principal fique bloqueada por muito tempo, divida uma tarefa longa em várias menores.

Isso é importante porque, quando as tarefas são divididas, o navegador pode responder a trabalhos de maior prioridade muito mais rápido, incluindo interações do usuário. Depois disso, as tarefas restantes são executadas até a conclusão, garantindo que o trabalho que você colocou na fila inicialmente seja feito.

Na parte de cima da figura anterior, um manipulador de eventos enfileirado por uma interação do usuário precisou esperar uma única tarefa longa antes de começar. Isso atrasa a interação. Nesse cenário, o usuário pode ter notado um atraso. Na parte de baixo, o manipulador de eventos pode começar a ser executado mais cedo, e a interação pode ter parecido instantânea.
Agora que você sabe por que é importante dividir as tarefas, aprenda a fazer isso em JavaScript.
Estratégias de gerenciamento de tarefas
Um conselho comum na arquitetura de software é dividir o trabalho em funções menores:
function saveSettings () {
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
Neste exemplo, há uma função chamada saveSettings()
que chama cinco funções para validar um formulário, mostrar um spinner, enviar dados para o back-end do aplicativo, atualizar a interface do usuário e enviar análises.
Conceitualmente, o saveSettings()
é bem arquitetado. Se você precisar depurar uma dessas funções, percorra a árvore de projetos para descobrir o que cada uma faz. Dividir o trabalho assim facilita a navegação e a manutenção dos projetos.
No entanto, um possível problema é que o JavaScript não executa cada uma dessas funções como tarefas separadas, porque elas são executadas na função saveSettings()
. Isso significa que todas as cinco funções serão executadas como uma só tarefa.

saveSettings()
que chama cinco funções. O trabalho é executado como parte de uma longa tarefa monolítica, bloqueando qualquer resposta visual até que todas as cinco funções sejam concluídas.
Na melhor das hipóteses, apenas uma dessas funções pode contribuir com 50 milissegundos ou mais para a duração total da tarefa. No pior caso, mais dessas tarefas podem ser executadas por muito mais tempo, especialmente em dispositivos com poucos recursos.
Nesse caso, saveSettings()
é acionado por um clique do usuário. Como o navegador não consegue mostrar uma resposta até que toda a função seja executada, o resultado dessa tarefa longa é uma interface lenta e sem resposta, que será medida como uma Interação até a próxima renderização (INP) ruim.
Adiar manualmente a execução do código
Para garantir que as tarefas importantes voltadas ao usuário e as respostas da interface aconteçam antes das tarefas de baixa prioridade, ceda à linha de execução principal interrompendo brevemente seu trabalho para dar ao navegador a oportunidade de executar tarefas mais importantes.
Um método usado pelos desenvolvedores para dividir tarefas em menores envolve setTimeout()
. Com essa técnica, você transmite a função para setTimeout()
. Isso adia a execução do callback para uma tarefa separada, mesmo que você especifique um tempo limite de 0
.
function saveSettings () {
// Do critical work that is user-visible:
validateForm();
showSpinner();
updateUI();
// Defer work that isn't user-visible to a separate task:
setTimeout(() => {
saveToDatabase();
sendAnalytics();
}, 0);
}
Isso é conhecido como geração e funciona melhor para uma série de funções que precisam ser executadas em sequência.
No entanto, nem sempre o código está organizado dessa forma. Por exemplo, você pode ter uma grande quantidade de dados que precisam ser processados em um loop, e essa tarefa pode levar muito tempo se houver muitas iterações.
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
Usar setTimeout()
aqui é problemático devido à ergonomia do desenvolvedor. Depois de cinco rodadas de setTimeout()
s aninhados, o navegador começa a impor um atraso mínimo de 5 milissegundos para cada setTimeout()
adicional.
O setTimeout
também tem outra desvantagem quando se trata de geração: quando você gera para a linha de execução principal adiando a execução do código em uma tarefa subsequente usando setTimeout
, essa tarefa é adicionada ao final da fila. Se houver outras tarefas aguardando, elas serão executadas antes do seu código adiado.
Uma API de geração dedicada: scheduler.yield()
scheduler.yield()
é uma API projetada especificamente para ceder à linha de execução principal no navegador.
Não é uma sintaxe no nível da linguagem nem um constructo especial. scheduler.yield()
é apenas uma função que retorna um Promise
que será resolvido em uma tarefa futura. Qualquer código encadeado para ser executado depois que esse Promise
for resolvido (em uma cadeia .then()
explícita ou depois de await
em uma função assíncrona) será executado nessa tarefa futura.
Na prática: insira um await scheduler.yield()
e a função vai pausar a execução nesse ponto e ceder à linha de execução principal. A execução do restante da função, chamada de continuação, será programada para ser executada em uma nova tarefa de loop de eventos. Quando essa tarefa começar, a promessa aguardada será resolvida, e a função vai continuar a execução de onde parou.
async function saveSettings () {
// Do critical work that is user-visible:
validateForm();
showSpinner();
updateUI();
// Yield to the main thread:
await scheduler.yield()
// Work that isn't user-visible, continued in a separate task:
saveToDatabase();
sendAnalytics();
}

saveSettings()
agora é dividida em duas tarefas. Como resultado, o layout e a renderização podem ser executados entre as tarefas, ao usuário uma resposta visual mais rápida, medida pela interação do ponteiro, que agora é muito mais curta.
No entanto, o benefício real do scheduler.yield()
em relação a outras abordagens de geração é que a continuação dele é priorizada. Isso significa que, se você gerar no meio de uma tarefa, a continuação da tarefa atual será executada antes de qualquer outra tarefa semelhante ser iniciada.
Isso evita que o código de outras fontes de tarefas interrompa a ordem de execução do seu código, como tarefas de scripts de terceiros.

scheduler.yield()
, a continuação retoma de onde parou antes de passar para outras tarefas.
Suporte para vários navegadores
scheduler.yield()
ainda não é compatível com todos os navegadores, então um substituto é necessário.
Uma solução é soltar o scheduler-polyfill
na sua build. Assim, o scheduler.yield()
pode ser usado diretamente. O polyfill vai processar o retorno a outras funções de programação de tarefas para que ele funcione de maneira semelhante em todos os navegadores.
Como alternativa, uma versão menos sofisticada pode ser escrita em algumas linhas, usando apenas setTimeout
envolvido em uma promessa como substituto se scheduler.yield()
não estiver disponível.
function yieldToMain () {
if (globalThis.scheduler?.yield) {
return scheduler.yield();
}
// Fall back to yielding with setTimeout.
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
Embora os navegadores sem suporte a scheduler.yield()
não recebam a continuação priorizada, eles ainda vão gerar para que o navegador permaneça responsivo.
Por fim, pode haver casos em que seu código não pode ceder à linha de execução principal se a continuação não for priorizada. Por exemplo, uma página conhecida como ocupada em que a cedência corre o risco de não concluir o trabalho por algum tempo. Nesse caso, scheduler.yield()
pode ser tratado como um tipo de melhoria progressiva: gerar nos navegadores em que scheduler.yield()
está disponível, caso contrário, continuar.
Isso pode ser feito detectando recursos e voltando a esperar uma única microtarefa em uma linha conveniente:
// Yield to the main thread if scheduler.yield() is available.
await globalThis.scheduler?.yield?.();
Divida o trabalho de longa duração com scheduler.yield()
O benefício de usar qualquer um desses métodos de scheduler.yield()
é que você pode await
em qualquer função async
.
Por exemplo, se você tiver uma matriz de jobs para executar que geralmente acabam se somando a uma tarefa longa, insira yields para dividir a tarefa.
async function runJobs(jobQueue) {
for (const job of jobQueue) {
// Run the job:
job();
// Yield to the main thread:
await yieldToMain();
}
}
A continuação de runJobs()
será priorizada, mas ainda permitirá que trabalhos de maior prioridade, como responder visualmente à entrada do usuário, sejam executados sem precisar esperar que a lista potencialmente longa de jobs seja concluída.
No entanto, esse não é um uso eficiente de geração. O scheduler.yield()
é rápido e eficiente, mas tem alguma sobrecarga. Se alguns dos jobs em jobQueue
forem muito curtos, a sobrecarga poderá se acumular rapidamente e resultar em mais tempo gasto gerando e retomando do que executando o trabalho real.
Uma abordagem é agrupar os jobs, gerando entre eles apenas se já tiver passado tempo suficiente desde a última geração. Um prazo comum é de 50 milissegundos para evitar que as tarefas se tornem longas, mas ele pode ser ajustado como uma compensação entre capacidade de resposta e tempo para concluir a fila de jobs.
async function runJobs(jobQueue, deadline=50) {
let lastYield = performance.now();
for (const job of jobQueue) {
// Run the job:
job();
// If it's been longer than the deadline, yield to the main thread:
if (performance.now() - lastYield > deadline) {
await yieldToMain();
lastYield = performance.now();
}
}
}
O resultado é que os jobs são divididos para nunca demorarem muito para serem executados, mas o executor só cede à linha de execução principal a cada 50 milissegundos.

Não use isInputPending()
A API isInputPending()
oferece uma maneira de verificar se um usuário tentou interagir com uma página e só gera se uma entrada estiver pendente.
Isso permite que o JavaScript continue se não houver entradas pendentes, em vez de gerar e acabar na parte de trás da fila de tarefas. Isso pode resultar em melhorias impressionantes de desempenho, conforme detalhado na Intent to Ship (link em inglês), para sites que, de outra forma, não retornariam à linha de execução principal.
No entanto, desde o lançamento dessa API, nossa compreensão sobre o rendimento aumentou, principalmente com a introdução do INP. Não recomendamos mais o uso dessa API. Em vez disso, recomendamos gerar independente de haver ou não uma entrada pendente por vários motivos:
isInputPending()
pode retornarfalse
incorretamente, mesmo que um usuário tenha interagido em algumas circunstâncias.- A entrada não é o único caso em que as tarefas precisam ser geradas. Animações e outras atualizações regulares da interface do usuário podem ser igualmente importantes para fornecer uma página da Web responsiva.
- Desde então, foram introduzidas APIs de geração mais abrangentes que abordam problemas de geração, como
scheduler.postTask()
escheduler.yield()
.
Conclusão
Gerenciar tarefas é desafiador, mas garante que sua página responda mais rapidamente às interações do usuário. Não existe uma única dica para gerenciar e priorizar tarefas, mas sim várias técnicas diferentes. Para reiterar, estas são as principais coisas a serem consideradas ao gerenciar tarefas:
- Ceda à linha de execução principal para tarefas críticas voltadas ao usuário.
- Use
scheduler.yield()
(com um fallback entre navegadores) para gerar e receber continuações priorizadas de forma ergonômica. - Por fim, faça o mínimo de trabalho possível nas funções.
Para saber mais sobre scheduler.yield()
, a scheduler.postTask()
relativa de programação de tarefas explícita e a priorização de tarefas, consulte os documentos da API Prioritized Task Scheduling.
Com uma ou mais dessas ferramentas, você pode estruturar o trabalho no seu aplicativo para priorizar as necessidades do usuário e garantir que o trabalho menos crítico ainda seja feito. Isso vai criar uma experiência do usuário melhor, mais responsiva e mais agradável de usar.
Agradecemos especialmente a Philip Walton pela avaliação técnica deste guia.
Imagem em miniatura de Unsplash, cortesia de Amirali Mirhashemian.