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 ao comando 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 antes do que se tivessem que esperar a conclusão de tarefas longas.
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 esse grande pedaç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. 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 do rendimento explícito pode acabar sendo 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:
- 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
. - 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ê quer retomar imediatamente após o rendimento não ficará para trás 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 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:
- Navegue para
chrome://flags
. - Ative o experimento Recursos experimentais da plataforma da Web. Talvez seja necessário reiniciar o Chrome depois disso.
- Navegue até a página de demonstração ou use a versão incorporada abaixo desta lista.
- Clique no botão superior Executar tarefas periodicamente.
- 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:
- Se você quiser testar o
scheduler.yield
localmente, digite e digitechrome://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á oscheduler.yield
(e qualquer outro recurso experimental) somente na sua instância do Chrome. - 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 doscheduler.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:
- Você já está usando
scheduler.postTask
no seu aplicativo para programar tarefas. - Você quer ser capaz de definir tarefas e ceder prioridades.
- É possível cancelar ou alterar a prioridade de tarefas usando a classe
TaskController
oferecida pela APIscheduler.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.