Apresentação do teste de origem agendador.yield

Criar sites que respondam rapidamente às entradas dos usuários tem sido um dos aspectos mais desafiadores do desempenho da Web. A equipe do Chrome tem trabalhado arduamente para ajudar os desenvolvedores da Web. Ainda este ano, foi anunciado que a métrica Interação com a próxima exibição (INP, na sigla em inglês) passaria do status experimental para o pendente. Ela vai substituir a latência na primeira entrada (FID, na sigla em inglês) como uma Core Web Vitals em março de 2024.

Em um esforço contínuo para fornecer novas APIs que ajudam os desenvolvedores da Web a tornar os sites o mais ágeis possíveis, a equipe do Chrome está fazendo um teste de origem do scheduler.yield a partir da versão 115 do Chrome. O scheduler.yield é uma nova adição proposta à API do programador que permite uma maneira mais fácil e melhor de retornar o controle à linha de execução principal do que os métodos tradicionalmente usados.

No rendimento

O JavaScript usa o modelo da execução até a conclusão para lidar com as tarefas. Isso significa que, quando uma tarefa é executada na linha de execução principal, ela é executada pelo tempo necessário para ser concluída. Após a conclusão de uma tarefa, o controle é gerado de volta para a linha de execução principal, o que permite que ela processe a próxima tarefa na fila.

Além de casos extremos em que uma tarefa nunca termina, como um loop infinito, por exemplo, o rendimento é um aspecto inevitável da lógica de agendamento de tarefas do JavaScript. Isso vai acontecer, é apenas uma questão de quando e, mais cedo ou mais tarde. As tarefas demoram muito para serem executadas (mais de 50 milissegundos, para ser mais exato) são consideradas tarefas longas.

Tarefas longas são uma fonte de baixa capacidade de resposta da página, porque atrasam a capacidade do navegador de responder à entrada do usuário. Quanto mais as tarefas longas ocorrerem (e quanto mais tempo elas forem executadas), maior será a probabilidade de os usuários terem a impressão de que a página está lenta, ou até mesmo de sentir que ela está completamente quebrada.

No entanto, só porque seu código inicia uma tarefa no navegador não significa que você precisa esperar até que essa tarefa seja concluída antes que o controle volte à linha de execução principal. Para melhorar a capacidade de resposta a entradas do usuário em uma página, renda explicitamente uma tarefa, o que divide a tarefa para ser concluída na próxima oportunidade disponível. Isso permite que outras tarefas recebam tempo na linha de execução principal mais cedo do que se precisassem esperar a conclusão de tarefas longas.

Uma representação de como dividir uma tarefa pode facilitar a melhor capacidade de resposta de entrada. Na parte superior, uma tarefa longa impede que um manipulador de eventos seja executado até que a tarefa seja concluída. Na parte de baixo, a tarefa dividida permite que o manipulador de eventos seja executado antes do que teria sido feito de outra forma.
Visualização da liberação do controle de volta para a linha de execução principal. Na parte superior, o rendimento ocorre somente depois que uma tarefa é executada até a conclusão, o que significa que as tarefas podem levar mais tempo para serem concluídas antes de retornar o controle de volta à linha de execução principal. No fundo, o rendimento é feito explicitamente, dividindo uma tarefa longa em várias menores. Isso permite que as interações do usuário sejam executadas mais rapidamente, o que melhora a capacidade de resposta de entrada e o INP.

Ao fazer isso explicitamente, você está dizendo ao navegador: "Ei, entendo que o trabalho que estou prestes a fazer pode levar um tempo, e não quero que você faça todo esse trabalho antes de responder à entrada do usuário ou a outras tarefas que também possam ser importantes. É uma ferramenta valiosa para desenvolvedores que pode ajudar muito a melhorar a experiência do usuário.

O problema com as estratégias de rendimento atuais

Um método comum de rendimento usa setTimeout com um valor de tempo limite de 0. Isso funciona porque o callback transmitido para setTimeout move o trabalho restante para uma tarefa separada que será colocada na fila para execução subsequente. Em vez de esperar que o navegador produza por conta própria, você diz: "vamos dividir essa grande parte do trabalho em partes menores".

No entanto, o rendimento com setTimeout tem um efeito colateral potencialmente indesejável: o trabalho que vem após o ponto de rendimento vai para o fim da fila de tarefas. As tarefas agendadas por interações do usuário ainda vão para a frente da fila como deveriam, mas o trabalho restante que você queria fazer depois de gerar explicitamente pode acabar sendo atrasado por outras tarefas de fontes concorrentes que estavam na fila à frente.

Para ver isso em ação, teste esta demonstração do Glitch ou teste na versão incorporada abaixo. A demonstração consiste em alguns botões em que você pode clicar e uma caixa abaixo deles que registra quando as tarefas são executadas. Quando você acessar a página, faça o seguinte:

  1. Clique no botão de cima chamado Executar tarefas periodicamente, que programará a execução de tarefas de bloqueio de vez em quando. Quando você clicar nesse botão, o registro da tarefa será preenchido com várias mensagens que dizem Executar a tarefa de bloqueio com setInterval.
  2. Em seguida, clique no botão Run loop, gerando setTimeout em cada iteração.

A caixa na parte de baixo da demonstração ficará mais ou menos assim:

Processing loop item 1
Processing loop item 2
Ran blocking task via setInterval
Processing loop item 3
Ran blocking task via setInterval
Processing loop item 4
Ran blocking task via setInterval
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval

Essa saída demonstra o comportamento de "fim da fila de tarefas" que ocorre ao produzir com setTimeout. A repetição que executa processa cinco itens e gera setTimeout depois que cada um é processado.

Isso ilustra um problema comum na Web: não é incomum que um script, especialmente um script de terceiros, registre uma função de timer que executa o trabalho em algum intervalo. O comportamento de "fim da fila de tarefas" que ocorre com o rendimento com setTimeout significa que o trabalho de outras origens de tarefas pode ser colocado na fila antes do trabalho restante que o loop precisa fazer depois do rendimento.

Dependendo do seu aplicativo, isso pode ou não ser um resultado desejável. No entanto, em muitos casos, esse comportamento é o motivo pelo qual os desenvolvedores podem se sentir relutantes em abrir mão do controle da linha de execução principal tão facilmente. O rendimento é bom porque as interações do usuário têm a oportunidade de ser executadas antes, mas também permite que outras interações que não sejam do usuário também ganhem tempo na linha de execução principal. É um problema real, mas o scheduler.yield pode ajudar a resolvê-lo.

Insira scheduler.yield

O scheduler.yield está disponível atrás de uma sinalização como um recurso experimental de plataforma da Web desde a versão 115 do Chrome. Uma pergunta que você pode fazer é "Por que preciso de uma função especial para gerar o resultado se o setTimeout já faz isso?"

Vale ressaltar que o rendimento não era uma meta de design de setTimeout, mas um bom efeito colateral na programação de um callback para ser executado em um momento futuro, mesmo com um valor de tempo limite de 0 especificado. O que é mais importante de lembrar, no entanto, é que produzir com setTimeout envia o trabalho restante para o verso da fila de tarefas. Por padrão, scheduler.yield envia o trabalho restante para a parte da frente da fila. Isso significa que o trabalho que você quer retomar imediatamente após a produção não ficará em segundo plano nas tarefas de outras fontes (com a notável exceção das interações do usuário).

scheduler.yield é uma função que produz a linha de execução principal e retorna uma Promise quando chamada. Isso significa que você pode usar await em uma função async:

async function yieldy () {
  // Do some work...
  // ...

  // Yield!
  await scheduler.yield();

  // Do some more work...
  // ...
}

Para ver o scheduler.yield em ação, faça o seguinte:

  1. Navegue para chrome://flags.
  2. Ative o experimento Recursos experimentais da Plataforma Web. Talvez seja necessário reiniciar o Chrome depois disso.
  3. Navegue até a página de demonstração ou use a versão incorporada abaixo desta lista.
  4. Clique no botão de cima chamado Executar tarefas periodicamente.
  5. Por fim, clique no botão Run loop, gerando scheduler.yield em cada iteração.

O resultado na caixa na parte inferior da página será mais ou menos assim:

Processing loop item 1
Processing loop item 2
Processing loop item 3
Processing loop item 4
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval

Ao contrário da demonstração que gera usando setTimeout, você pode notar que a repetição, mesmo que produza após cada iteração, não envia o trabalho restante para a parte de trás da fila, mas para a frente dela. Com isso, você tem o melhor dos dois mundos: é possível melhorar a capacidade de resposta das entradas no seu site, mas também garantir que o trabalho que você queria terminar depois não atrasar.

Tente você também!

Se o scheduler.yield parecer interessante e você quiser testá-lo, há duas maneiras de fazer isso a partir da versão 115 do Chrome:

  1. Se você quiser testar o scheduler.yield localmente, digite e digite chrome://flags na barra de endereço do Chrome e selecione Ativar no menu suspenso da seção Recursos experimentais da plataforma Web. Isso fará com que o scheduler.yield (e outros recursos experimentais) fiquem disponíveis somente na sua instância do Chrome.
  2. Se você quiser ativar a scheduler.yield para usuários reais do Chromium em uma origem acessível publicamente, precisará se inscrever no teste de origem do scheduler.yield. Com isso, é possível testar com segurança os recursos propostos por um determinado período e oferecer à equipe do Chrome insights valiosos sobre como esses recursos são usados em campo. Para saber mais sobre como os testes de origem funcionam, leia este guia.

A forma como você usa o scheduler.yield, embora ainda seja compatível com navegadores que não o implementam, depende das suas metas. Você pode usar o polyfill oficial. O polyfill é útil se o seguinte se aplicar à sua situação:

  1. Você já está usando o scheduler.postTask no seu aplicativo para programar tarefas.
  2. Você quer ser capaz de definir tarefas e prioridades de produção.
  3. Você pode cancelar ou mudar a prioridade de tarefas usando a classe TaskController oferecida pela API scheduler.postTask.

Se isso não descrever sua situação, talvez o polyfill não seja adequado para você. Nesse caso, você pode implementar seu próprio substituto de algumas maneiras. A primeira abordagem usa scheduler.yield se ele estiver disponível, mas volta para setTimeout se não estiver:

// A function for shimming scheduler.yield and setTimeout:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to setTimeout:
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

Isso pode funcionar, mas, como você pode imaginar, os navegadores que não oferecem suporte a scheduler.yield resultarão sem o comportamento "na frente da fila". Se isso significar que você prefere não gerar nada, tente outra abordagem que use scheduler.yield se estiver disponível, mas não vai produzir se não estiver:

// A function for shimming scheduler.yield with no fallback:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to nothing:
  return;
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

O scheduler.yield é uma adição interessante à API do programador. Com ela, será mais fácil para os desenvolvedores melhorar a capacidade de resposta do que as estratégias de rendimento atuais. Se scheduler.yield parece ser uma API útil para você, participe da nossa pesquisa para ajudar a melhorá-la e envie feedback sobre como ela pode ser aprimorada.

Imagem principal do Unsplash, de Jonathan Allison.