Usar scheduler.yield() para dividir tarefas longas

Brendan Kenny
Brendan Kenny

Publicado em: 6 de março de 2025

Browser Support

  • Chrome: 129.
  • Edge: 129.
  • Firefox: 142.
  • Safari: not supported.

Source

Uma página fica lenta e sem resposta quando tarefas longas mantêm a linha de execução principal ocupada, impedindo que ela faça outros trabalhos importantes, como responder à entrada do usuário. Como resultado, até mesmo controles de formulário integrados podem parecer quebrados para os usuários, como se a página estivesse congelada, sem falar em componentes personalizados mais complexos.

scheduler.yield() é uma maneira de ceder à linha de execução principal, permitindo que o navegador execute qualquer trabalho pendente de alta prioridade e continue a execução de onde parou. Isso mantém uma página mais responsiva e, por sua vez, ajuda a melhorar a interação com a próxima exibição (INP).

O scheduler.yield oferece uma API ergonômica que faz exatamente o que diz: a execução da função em que ela é chamada é pausada na expressão await scheduler.yield() e cede à linha de execução principal, dividindo a tarefa. A execução do restante da função, chamada de continuação da função, será programada para ser executada em uma nova tarefa de loop de eventos.

async function respondToUserClick() {
  giveImmediateFeedback();
  await scheduler.yield(); // Yield to the main thread.
  slowerComputation();
}

O benefício específico de scheduler.yield é que a continuação após o yield é programada para ser executada antes de qualquer outra tarefa semelhante que tenha sido enfileirada pela página. Ele prioriza a continuação de uma tarefa em vez de iniciar novas.

Funções como setTimeout ou scheduler.postTask também podem ser usadas para dividir tarefas, mas essas continuações geralmente são executadas depois de novas tarefas já enfileiradas, o que pode causar longos atrasos entre a transferência para a linha de execução principal e a conclusão do trabalho.

Continuações priorizadas após a geração

scheduler.yield faz parte da API Prioritized Task Scheduling. Como desenvolvedores da Web, geralmente não falamos sobre a ordem em que o loop de eventos executa tarefas em termos de prioridades explícitas, mas as prioridades relativas estão sempre lá, como um callback requestIdleCallback sendo executado após qualquer callback setTimeout enfileirado ou um listener de eventos de entrada acionado geralmente sendo executado antes de uma tarefa enfileirada com setTimeout(callback, 0).

O agendamento de tarefas priorizadas apenas torna isso mais explícito, facilitando a descoberta de qual tarefa será executada antes de outra, e permite ajustar as prioridades para mudar essa ordem de execução, se necessário.

Como mencionado, a execução contínua de uma função depois de gerar com scheduler.yield() tem uma prioridade maior do que iniciar outras tarefas. O conceito principal é que a continuação de uma tarefa deve ser executada primeiro, antes de passar para outras tarefas. Se a tarefa for um código bem-comportado que gera periodicamente para que o navegador possa fazer outras coisas importantes (como responder à entrada do usuário), ela não deve ser penalizada por gerar, sendo priorizada após outras tarefas semelhantes.

Exemplo: duas funções enfileiradas para serem executadas em tarefas diferentes usando setTimeout.

setTimeout(myJob);
setTimeout(someoneElsesJob);

Nesse caso, as duas chamadas setTimeout estão lado a lado, mas em uma página real, elas podem ser chamadas em lugares completamente diferentes, como um script próprio e um script de terceiros configurando o trabalho para ser executado de forma independente. Também podem ser duas tarefas de componentes separados sendo acionadas no agendador do seu framework.

Veja como esse trabalho pode ser feito no DevTools:

Duas tarefas mostradas no painel de performance do Chrome DevTools. Ambas são indicadas como tarefas longas. A função "myJob" ocupa toda a execução da primeira tarefa, e "someoneElsesJob" ocupa toda a segunda tarefa.

myJob é sinalizada como uma tarefa longa, impedindo que o navegador faça qualquer outra coisa enquanto ela está em execução. Supondo que seja de um script próprio, podemos dividi-lo:

function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with setTimeout() to break up long task, then run part2.
  setTimeout(myJobPart2, 0);
}

Como myJobPart2 foi programado para ser executado com setTimeout em myJob, mas essa programação é executada depois que someoneElsesJob já foi programado, veja como será a execução:

Três tarefas mostradas no painel de performance do Chrome DevTools. A primeira é a execução da função "myJobPart1", a segunda é uma tarefa longa que executa "someoneElsesJob" e, por fim, a terceira tarefa é a execução de "myJobPart2".

Dividimos a tarefa com setTimeout para que o navegador possa responder durante o meio de myJob, mas agora a segunda parte de myJob só é executada depois que someoneElsesJob termina.

Em alguns casos, isso pode ser bom, mas geralmente não é o ideal. myJob estava cedendo à linha de execução principal para garantir que a página permanecesse responsiva à entrada do usuário, não para desistir da linha de execução principal completamente. Em casos em que someoneElsesJob é especialmente lento ou muitos outros jobs além de someoneElsesJob também foram programados, pode demorar muito para que a segunda metade de myJob seja executada. Provavelmente não era essa a intenção do desenvolvedor ao adicionar setTimeout a myJob.

Insira scheduler.yield(), que coloca a continuação de qualquer função que o invoque em uma fila de prioridade um pouco mais alta do que o início de qualquer outra tarefa semelhante. Se myJob for alterado para usar:

async function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with scheduler.yield() to break up long task, then run part2.
  await scheduler.yield();
  myJobPart2();
}

Agora a execução vai ficar assim:

Duas tarefas mostradas no painel de performance do Chrome DevTools. Ambas são indicadas como tarefas longas. A função "myJob" ocupa toda a execução da primeira tarefa, e "someoneElsesJob" ocupa toda a segunda tarefa.

O navegador ainda tem a oportunidade de ser responsivo, mas agora a continuação da tarefa myJob é priorizada em vez de iniciar a nova tarefa someoneElsesJob. Assim, myJob é concluída antes de someoneElsesJob começar. Isso é muito mais próximo da expectativa de ceder à linha de execução principal para manter a capacidade de resposta, sem desistir totalmente dela.

Herança de prioridade

Como parte da API Prioritized Task Scheduling maior, scheduler.yield() funciona bem com as prioridades explícitas disponíveis em scheduler.postTask(). Sem uma prioridade definida explicitamente, um scheduler.yield() em um callback scheduler.postTask() vai agir basicamente da mesma forma que no exemplo anterior.

No entanto, se uma prioridade for definida, como o uso de uma prioridade 'background' baixa:

async function lowPriorityJob() {
  part1();
  await scheduler.yield();
  part2();
}

scheduler.postTask(lowPriorityJob, {priority: 'background'});

A continuação será agendada com uma prioridade maior do que outras tarefas 'background', recebendo a continuação priorizada esperada antes de qualquer trabalho 'background' pendente, mas ainda com uma prioridade menor do que outras tarefas padrão ou de alta prioridade. Ela continua sendo um trabalho 'background'.

Isso significa que, se você programar um trabalho de baixa prioridade com um 'background' scheduler.postTask() (ou com requestIdleCallback), a continuação após um scheduler.yield() também vai aguardar até que a maioria das outras tarefas seja concluída e a linha de execução principal esteja inativa para ser executada. É exatamente isso que você quer de um trabalho de baixa prioridade.

Como usar a API

Por enquanto, o scheduler.yield() está disponível apenas em navegadores baseados no Chromium. Para usá-lo, você precisa detectar recursos e voltar para uma maneira secundária de gerar para outros navegadores.

scheduler-polyfill é um pequeno polyfill para scheduler.postTask e scheduler.yield que usa internamente uma combinação de métodos para emular grande parte do poder das APIs de programação em outros navegadores, embora a herança de prioridade scheduler.yield() não seja compatível.

Para quem quer evitar um polyfill, um método é usar setTimeout() e aceitar a perda de uma continuação priorizada ou até mesmo não usar em navegadores sem suporte, se isso não for aceitável. Consulte a documentação do scheduler.yield() em "Otimizar tarefas longas" para mais informações.

Os tipos wicg-task-scheduling também podem ser usados para receber verificação de tipo e suporte do ambiente de desenvolvimento integrado se você estiver detectando recursos scheduler.yield() e adicionando um substituto por conta própria.

Saiba mais

Para mais informações sobre a API e como ela interage com prioridades de tarefas e scheduler.postTask(), confira os documentos scheduler.yield() e Prioritized Task Scheduling na MDN.

Para saber mais sobre tarefas longas, como elas afetam a experiência do usuário e o que fazer a respeito, leia sobre otimização de tarefas longas.