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 a atender a esse requisito. Ainda este ano, foi anunciado que a métrica Interaction to Next Paint (INP) passaria de experimental para 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.
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. 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, só porque o código inicia uma tarefa no navegador não significa que você precisa esperar até que ela seja concluída antes de retornar o controle para a linha de execução principal. É possível melhorar a capacidade de resposta à entrada do usuário em uma página ao renderizar explicitamente em 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 que as tarefas longas fossem concluídas.
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 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
move o trabalho restante para uma tarefa separada que será enfileirada para execução posterior. Em vez de esperar que o navegador seja renderizado, você diz "vamos dividir esse grande bloco 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 conferir isso em ação, teste esta demonstração do Glitch ou experimente a 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:
- 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 Ran blocking task with
setInterval
. - 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 de "fim da fila de tarefas" que ocorre ao renderizar com setTimeout
. O loop que é executado processa cinco itens e gera setTimeout
depois que cada um deles é 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 "fim da fila de tarefas" que vem com a rendição 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 rendição.
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 você pode 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:
- Navegue para
chrome://flags
. - Ative o experimento Recursos experimentais da plataforma da Web. Talvez seja necessário reiniciar o Chrome depois disso.
- Acesse a página de demonstração ou use a versão incorporada abaixo desta lista.
- Clique no botão de cima Executar tarefas periodicamente.
- Por fim, clique no botão Run loop, yielding with
scheduler.yield
on each iteration.
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 gera usando setTimeout
, o loop não envia o trabalho restante para a parte de trás da fila, mas para a parte da frente, mesmo que gere após cada iteração. 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 terminar depois da cessão não seja atrasado.
Experimente!
Se o scheduler.yield
parecer interessante e você quiser testar, há duas maneiras de fazer isso a partir da versão 115 do Chrome:
- Se você quiser testar o
scheduler.yield
localmente, digite e insirachrome://flags
na barra de endereço do Chrome e selecione Ativar no menu suspenso na seção Recursos experimentais da plataforma da Web. Isso vai disponibilizarscheduler.yield
(e outros recursos experimentais) apenas na sua instância do Chrome. - 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 doscheduler.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:
- Você já usa
scheduler.postTask
no seu aplicativo para programar tarefas. - Você quer definir prioridades de tarefas e rendimento.
- Você quer cancelar ou priorizar tarefas usando a classe
TaskController
oferecida pela APIscheduler.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 melhorada.
Imagem principal do Unsplash, por Jonathan Allison.