Apresentação do teste de origem agendador.yield

Criar sites que respondem rapidamente à entrada do usuário é um dos aspectos mais desafiadores da performance da Web. A equipe do Chrome está trabalhando muito para ajudar os desenvolvedores da Web. Ainda este ano, foi anunciado que a métrica "Interaction to Next Paint" (INP) passaria de experimental para status pendente. Agora, ela vai substituir a First Input Delay (FID) como uma Core Web Vital em março de 2024.

Em um esforço contínuo para oferecer novas APIs que ajudem os desenvolvedores da Web a tornar os sites mais rápidos, a equipe do Chrome está executando um teste de origem para scheduler.yield a partir da versão 115 do Chrome. scheduler.yield é uma nova adição proposta à API do programador, que permite uma maneira mais fácil e melhor de devolver 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 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 é entregue de volta à 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 é concluída, como um loop infinito, por exemplo, o rendimento é um aspecto inevitável da lógica de programação de tarefas do JavaScript. Isso vai acontecer, é só uma questão de quando, e quanto antes, melhor. Quando as tarefas demoram muito para serem executadas, mais de 50 milissegundos para ser exato, elas são consideradas 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 frequentes as tarefas longas ocorrerem e quanto mais tempo elas durarem, maior será a probabilidade de os usuários terem a impressão de que a página está lenta ou até mesmo que ela está totalmente corrompida.

No entanto, o fato de o código iniciar uma tarefa no navegador não significa que você terá que esperar até que a tarefa seja concluída antes que o controle volte à linha de execução principal. Você pode melhorar a capacidade de resposta da entrada do usuário em uma página cedendo explicitamente uma tarefa, o que divide a tarefa para ser concluída na próxima oportunidade disponível. Isso permite que outras tarefas tenham tempo na linha de execução principal mais cedo do que se tivessem que esperar a conclusão de tarefas longas.

Uma representação de como dividir uma tarefa pode facilitar a resposta da entrada. Na parte de cima, 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 mais cedo do que seria possível.
Uma visualização de como o controle é devolvido à linha de execução principal. Na parte de cima, o rendimento ocorre somente depois que uma tarefa é concluída, o que significa que as tarefas podem levar mais tempo para serem concluídas antes de retornar o controle para a linha de execução principal. Na parte de baixo, 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 da entrada e o INP.

Quando você cede explicitamente, está dizendo ao navegador: "Ei, entendo que o trabalho que vou fazer pode levar um tempo e não quero que você tenha que fazer tudo desse trabalho antes de responder à entrada do usuário ou outras tarefas que também podem ser importantes". É uma ferramenta valiosa na caixa de ferramentas do desenvolvedor 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 moverá o trabalho restante para uma tarefa separada que será colocada na fila para execução posterior. Em vez de esperar que o navegador produza por conta própria, você diz: "vamos dividir esse grande pedaço de 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 final da fila de tarefas. As tarefas programadas por interações do usuário ainda vão para a frente da fila, mas o trabalho restante que você queria fazer depois de ceder explicitamente pode acabar sendo atrasado por outras tarefas de origens concorrentes que foram enfileiradas antes dele.

Para ver como isso funciona, confira esta demonstração do Glitch ou faça experimentos com o recurso na versão incorporada abaixo. A demonstração consiste em alguns botões que podem ser clicados e uma caixa abaixo deles que registra quando as tarefas são executadas. Quando a página aparecer, faça o seguinte:

  1. Clique no botão superior Executar tarefas periodicamente, que vai programar a execução de tarefas de bloqueio a cada determinado período. Ao clicar nesse botão, o registro de tarefas vai ser preenchido com várias mensagens que dizem A tarefa de bloqueio foi executada com setInterval.
  2. Em seguida, clique no botão Run loop, yielding with setTimeout on each iteration.

A caixa na parte de baixo da demonstração vai mostrar algo como:

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 "fim da fila de tarefas" que ocorre ao renderizar com setTimeout. A repetição executada processa cinco itens e gera com setTimeout depois que cada um foi 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 é executada em um intervalo. O comportamento "fim da fila de tarefas" que vem com a geração de rendimento com setTimeout significa que o trabalho de outras fontes de tarefas pode ser enfileirado antes do trabalho restante que o loop precisa fazer após a geração de rendimento.

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

Entre em scheduler.yield

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

Vale ressaltar que o rendimento não era um objetivo de design de setTimeout, mas um bom efeito colateral na programação de um callback para execução no futuro, mesmo com um valor de tempo limite de 0 especificado. No entanto, o mais importante é lembrar que o rendimento com setTimeout envia o trabalho restante para a parte de trás da fila de tarefas. Por padrão, scheduler.yield envia o trabalho restante para a frente da fila. Isso significa que o trabalho que você queria retomar imediatamente após ceder não será deixado de lado para tarefas de outras fontes, com exceção das interações do usuário.

scheduler.yield é uma função que gera a linha de execução principal e retorna um Promise quando chamada. Isso significa que é possível usar await em uma função async:

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

  // Yield!
  await scheduler.yield();

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

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

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

A saída na caixa na parte de baixo da página vai ser semelhante a esta:

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 é produzida usando setTimeout, é possível observar que o loop, mesmo que seja gerado após cada iteração, não envia o trabalho restante para o fim da fila, mas para a frente. Isso oferece o melhor dos dois mundos: você pode ceder para melhorar a capacidade de resposta de entrada no seu site, mas também garantir que o trabalho que você queria concluir depois da cessão não seja atrasado.

Experimente!

Caso scheduler.yield pareça interessante e você queira experimentá-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 Google Chrome e selecione Ativar no menu suspenso da seção Recursos experimentais da plataforma da Web. Isso vai disponibilizar scheduler.yield (e outros recursos experimentais) apenas na sua instância do Chrome.
  2. Se você quiser ativar o scheduler.yield para usuários reais do Chromium em uma origem acessível publicamente, precisará se inscrever no teste de origem do scheduler.yield. Isso permite que você teste com segurança os recursos propostos por um determinado período e oferece à equipe do Chrome insights valiosos sobre como esses recursos são usados no campo. Para mais informações sobre como os testes de origem funcionam, leia este guia.

A forma como você usa scheduler.yield, mantendo a compatibilidade com navegadores que não o implementam, depende dos seus objetivos. Use o polyfill oficial. O polyfill é útil se o seguinte for aplicável à sua situação:

  1. Você já usa scheduler.postTask no seu aplicativo para programar tarefas.
  2. Você quer definir prioridades de tarefas e rendimento.
  3. Você quer cancelar ou priorizar tarefas usando a classe TaskController oferecida pela API scheduler.postTask.

Se isso não descrever sua situação, talvez o polyfill não seja para você. Nesse caso, é possível usar seu próprio substituto de algumas maneiras. A primeira abordagem usa scheduler.yield se ele estiver disponível, mas retorna 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 vão gerar o comportamento "na frente da fila". Se isso significa que você prefere não ceder, tente outra abordagem que use scheduler.yield se estiver disponível, mas não vai ceder 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:
  // ...
}

scheduler.yield é uma adição interessante à API do programador, que vai facilitar a melhoria da capacidade de resposta dos desenvolvedores em relação às estratégias de rendimento atuais. Se a scheduler.yield parecer uma API útil para você, participe da nossa pesquisa para ajudar a melhorar e envie feedback sobre como ela pode ser aprimorada.

Imagem principal do Unsplash, por Jonathan Allison.