Apresentação do teste de origem agendador.yield

Criar sites que respondam rapidamente à entrada do usuário tem sido um dos aspectos mais desafiadores da performance da Web, e a equipe do Chrome tem trabalhado muito para ajudar os desenvolvedores da Web a atender a essa demanda. Neste ano, foi anunciado que a métrica Interaction to Next Paint (INP) passaria do status experimental para pendente. Agora, ela está pronta para substituir o 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 ajudam os desenvolvedores da Web a tornar os sites o mais rápidos possível, 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 Scheduler que permite uma maneira mais fácil e melhor de devolver o controle à linha de execução principal do que os métodos que têm sido usados tradicionalmente.

Ao ceder

O JavaScript usa o modelo de 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. Quando uma tarefa é concluída, o controle é retornado à linha de execução principal, que pode processar a próxima tarefa na fila.

Além de casos extremos em que uma tarefa nunca termina, como um loop infinito, por exemplo, o yield é 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 levam muito tempo para serem executadas (mais de 50 milissegundos, para ser exato), elas 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 frequentes e longas forem as tarefas, maior será a probabilidade de os usuários terem a impressão de que a página está lenta ou até mesmo quebrada.

No entanto, só porque seu código inicia uma tarefa no navegador não significa que você precisa esperar até que ela seja concluída antes que o controle seja devolvido à linha de execução principal. É possível melhorar a capacidade de resposta à entrada do usuário em uma página gerando explicitamente em uma tarefa, o que a divide 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 das tarefas longas.

Uma representação de como dividir uma tarefa pode facilitar uma melhor capacidade de resposta de 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 de outra forma.
Uma visualização de como devolver o controle à linha de execução principal. Na parte de cima, a geração 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 à linha de execução principal. No final, a geração é feita explicitamente, dividindo uma tarefa longa em várias menores. Isso permite que as interações do usuário sejam executadas mais cedo, o que melhora a capacidade de resposta de entrada e o INP.

Quando você cede explicitamente, está dizendo ao navegador: "Entendo que o trabalho que estou prestes a fazer pode levar um tempo, e não quero que você precise fazer todo esse trabalho antes de responder à entrada do usuário ou a outras tarefas que também podem ser importantes". É uma ferramenta valiosa na caixa de ferramentas de um desenvolvedor que pode melhorar muito a experiência do usuário.

O problema com as estratégias de otimização de receita atuais

Um método comum de geração de 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á enfileirada para execução subsequente. Em vez de esperar que o navegador gere por conta própria, você está dizendo "divida esse grande bloco de trabalho em partes menores".

No entanto, gerar com setTimeout tem um efeito colateral potencialmente indesejável: o trabalho que vem depois do 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, como deveriam, mas o restante do trabalho que você queria fazer depois de ceder explicitamente pode acabar sendo ainda mais adiado por outras tarefas de fontes concorrentes que foram enfileiradas antes dele.

Para ver isso em ação, teste esta demonstração do Codepen ou experimente a versão incorporada a seguir. 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 a página aparecer, faça o seguinte:

  1. Clique no botão de cima chamado Executar tarefas periodicamente, que programa tarefas de bloqueio para serem executadas de tempos em tempos. Ao clicar nesse botão, o registro de tarefas será preenchido com várias mensagens que dizem Executou tarefa de bloqueio com setInterval.
  2. Em seguida, clique no botão Executar loop, gerando com setTimeout em cada iteração.

Você vai notar que a caixa na parte de baixo da demonstração vai mostrar algo 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 gerar com setTimeout. O loop que executa processa cinco itens e gera com setTimeout depois que cada um é processado.

Isso ilustra um problema comum na Web: não é incomum que um script, principalmente de terceiros, registre uma função de timer que executa o trabalho em algum intervalo. O comportamento de "fim da fila de tarefas" que vem com a geração de 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.

Dependendo do aplicativo, esse pode ser um resultado desejável ou não. No entanto, em muitos casos, esse comportamento é o motivo pelo qual os desenvolvedores podem hesitar em abrir mão do controle da linha de execução principal tão facilmente. O yield é bom porque as interações do usuário têm a oportunidade de serem executadas mais cedo, mas também permite que outros trabalhos que não são de interação do usuário tenham tempo na linha de execução principal. É um problema real, mas o scheduler.yield pode ajudar a resolver!

Entre em scheduler.yield

O scheduler.yield está disponível por trás de uma flag como um recurso experimental da plataforma da Web desde a versão 115 do Chrome. Uma dúvida que pode surgir é: "Por que preciso de uma função especial para gerar quando setTimeout já faz isso?"

Vale a pena observar que a geração não era um objetivo de design do setTimeout, mas sim um efeito colateral interessante ao programar um callback para ser executado em um momento posterior no futuro, mesmo com um valor de tempo limite de 0 especificado. No entanto, é mais importante lembrar que o uso de setTimeout envia o trabalho restante para a parte de trás da fila de tarefas. Por padrão, o scheduler.yield envia o trabalho restante para a frente da fila. Isso significa que o trabalho que você queria retomar imediatamente após a conclusão não vai ficar em segundo plano em relação a tarefas de outras fontes (com a exceção notável 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 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 teste 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 a seguir.
  4. Clique no botão de cima chamado Executar tarefas periodicamente.
  5. Por fim, clique no botão Executar loop, gerando com scheduler.yield em cada iteração.

A saída na caixa na parte de baixo da página será parecida com 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 gera usando setTimeout, é possível notar que o loop, mesmo gerando após cada iteração, não envia o trabalho restante para o final da fila, mas sim para o início dela. Assim, você tem o melhor dos dois mundos: pode ceder para melhorar a capacidade de resposta da entrada no seu site, mas também garantir que o trabalho que você queria concluir depois de ceder não seja adiado.

Faça um teste!

Se scheduler.yield parece interessante e você quer testar, é possível fazer isso de duas maneiras a partir da versão 115 do Chrome:

  1. Se quiser testar o scheduler.yield localmente, digite e insira chrome://flags na barra de endereço do Chrome e selecione Ativar no menu suspenso da seção Recursos experimentais da plataforma da Web. Isso vai disponibilizar o 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 ao público, inscreva-se no teste de origem do scheduler.yield. Isso permite que você teste com segurança os recursos propostos por um determinado período e dá à 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, sem deixar de oferecer suporte a navegadores que não o implementam, depende dos seus objetivos. Você pode usar o polyfill oficial. O polyfill é útil se o seguinte se aplica à sua situação:

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

Se essa não for sua situação, talvez o polyfill não seja para você. Nesse caso, é possível criar seu próprio substituto de algumas maneiras. A primeira abordagem usa scheduler.yield se 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ê deve imaginar, navegadores que não oferecem suporte a scheduler.yield vão gerar sem o comportamento "na frente da fila". Se isso significa que você prefere não gerar nada, tente outra abordagem que usa scheduler.yield se estiver disponível, mas não gera nada 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 Scheduler, que esperamos facilitar a melhoria da capacidade de resposta para os desenvolvedores em comparação com as estratégias de geração atuais. Se scheduler.yield parece 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, de Jonathan Allison.