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 e que a equipe do Chrome tem trabalhado duro para ajudar os desenvolvedores da Web a atender. Ainda este ano, foi anunciado que a métrica Interaction to Next Paint (INP) passaria de experimental para status pendente. Ela vai substituir a First Input Delay (FID) como Core Web Vitals em março de 2024.

Em um esforço contínuo para fornecer novas APIs que ajudem os desenvolvedores Web a tornar os sites o mais rápidos possível, a equipe do Chrome está realizando um teste de origem do 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 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 é produzido de volta para a linha de execução principal, o que permite que a linha de execução principal processe a próxima tarefa na fila.

Exceto em casos extremos em que uma tarefa nunca termina, como em um loop infinito, o rendimento é um aspecto inevitável da lógica de programação de tarefas do JavaScript. Isso acontecerá, é só uma questão de quando e antes é melhor. Quando tarefas demoram muito para serem executadas (mais de 50 milissegundos, para ser mais 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 maior for a frequência das tarefas longas (e quanto mais tempo elas são executadas), maior será a probabilidade dos usuários terem a impressão de que a página está lenta ou até mesmo sentirem 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 ganhem 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 uma 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 inferior, a tarefa dividida permite que o manipulador de eventos seja executado antes do que teria.
Uma visualização da produção de controle de volta para a linha de execução principal. No topo, a produção ocorre somente após a conclusão de uma tarefa, 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. Na base, 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.

Ao ceder explicitamente, você estará dizendo ao navegador: "Entendo que o trabalho que estou prestes a fazer pode demorar um pouco e não quero que você tenha que 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 do desenvolvedor e que pode melhorar muito a experiência do usuário.

O problema com as estratégias atuais de rendimento

Um método comum de produção 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 essa grande porção de trabalho em partes menores".

No entanto, a produção com setTimeout causa um efeito colateral possivelmente indesejável: o trabalho que vem depois do ponto de rendimento vai para o fim da fila de tarefas. 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 do rendimento explícito pode acabar sendo mais atrasado por outras tarefas de fontes concorrentes que foram enfileiradas antes.

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 em que você pode clicar e uma caixa abaixo deles que registra quando as tarefas são executadas. Ao acessar a página, faça o seguinte:

  1. Clique no botão superior Executar tarefas periodicamente, que programará tarefas de bloqueio para serem executadas de tempos em tempos. Quando você clicar nesse botão, o registro de tarefas será preenchido com várias mensagens com a mensagem Executou a tarefa de bloqueio com setInterval.
  2. Em seguida, clique no botão Executar loop, produzindo com setTimeout em cada iteração.

Você verá que a caixa na parte inferior da demonstração será 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

Esta saída demonstra o "fim da fila de tarefas" comportamento que ocorre ao produzir 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 "fim da fila de tarefas" O comportamento associado à produção com setTimeout significa que o trabalho de outras fontes de tarefas pode ficar na fila antes do trabalho restante que o loop precisa fazer após a produção.

Dependendo do 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 ficar relutantes em abrir mão do controle da linha de execução principal. Gerar resultados é bom porque as interações do usuário podem ser executadas mais rapidamente, mas também permite que outros trabalhos de interação que não sejam do usuário ganhem tempo na linha de execução principal. É um problema real, mas o scheduler.yield pode ajudar a resolvê-lo.

Entre em scheduler.yield

O scheduler.yield está disponível atrás de 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 produzir se setTimeout já tem essa função?".

Vale ressaltar que o rendimento não era uma meta de design de setTimeout, mas um bom efeito colateral ao programar um callback para ser executado posteriormente, mesmo com um valor de tempo limite de 0 especificado. No entanto, é mais importante lembrar que a produção com setTimeout envia o trabalho restante para o back 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 o rendimento não ficará recuado para 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 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 ver 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. Navegue até a página de demonstração ou use a versão incorporada abaixo desta lista.
  4. Clique no botão superior 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 inferior da página 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 é gerada usando setTimeout, é possível observar que o loop, mesmo que seja produzido após cada iteração, não envia o trabalho restante para o fim da fila, mas para a frente dele. Isso oferece o melhor dos dois mundos: você pode melhorar a capacidade de resposta a entradas no seu site, mas também garantir que o trabalho que quer finalizar após o rendimento não atrasar.

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 disponibilizará o scheduler.yield (e qualquer outro recurso experimental) somente 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. Assim, você testa com segurança os recursos propostos em um determinado período e oferece à equipe do Chrome insights valiosos sobre como eles são usados em campo. Para mais informações sobre como os testes de origem funcionam, leia este guia.

A forma como você usa o scheduler.yield, embora ainda ofereça suporte aos navegadores que não o implementam, depende das suas metas. Você pode usar o polyfill oficial. O polyfill será útil se o seguinte se aplicar à sua situação:

  1. Você já está usando scheduler.postTask no seu aplicativo para programar tarefas.
  2. Você quer ser capaz de definir tarefas e ceder prioridades.
  3. É possível cancelar ou alterar a prioridade de tarefas usando a classe TaskController oferecida pela API scheduler.postTask.

Se isso não descreve sua situação, o polyfill pode não ser adequado para você. Nesse caso, você pode lançar seu próprio substituto de duas maneiras. A primeira abordagem vai usar scheduler.yield se estiver disponível, mas voltará 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 são compatíveis com scheduler.yield resultarão sem "na frente da fila" do seu modelo. Se isso significar que você prefere não gerar resultados, tente outra abordagem que use scheduler.yield, se disponível, mas não vai gerar resultados 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 scheduler. Esperamos que esse aumento seja mais fácil para os desenvolvedores melhorar a capacidade de resposta do que as estratégias de rendimento atuais. Se a scheduler.yield for uma API útil para você, participe da nossa pesquisa para aprimorá-la e envie feedback sobre como ela pode ser melhorada.

Imagem principal do Unsplash, por Jonathan Allison.