As dicas disponíveis para tornar seus apps JavaScript mais rápidos geralmente incluem "Não bloquear a linha de execução principal" e "Divida suas tarefas longas". Esta página detalha o que isso significa e por que a otimização de tarefas em JavaScript é importante.
O que é uma tarefa?
Uma tarefa é qualquer trabalho discreto executado pelo navegador. Isso inclui renderização, análise de HTML e CSS, execução do código JavaScript programado e outras coisas sobre as quais você pode não ter controle direto. O JavaScript das suas páginas é a principal fonte de tarefas do navegador.
As tarefas afetam o desempenho de várias maneiras. Por exemplo, quando um navegador faz o download de um arquivo JavaScript durante a inicialização, ele enfileira tarefas para analisar e compilar esse JavaScript para que ele possa ser executado. Mais tarde no ciclo de vida da página, outras tarefas começam quando o JavaScript funciona, como gerar interações por manipuladores de eventos, animações orientadas por JavaScript e atividades em segundo plano, como coleta de análise. Tudo isso, com exceção dos Web workers e APIs semelhantes, acontece na linha de execução principal.
Qual é a linha de execução principal?
A linha de execução principal é onde a maioria das tarefas é executada no navegador e onde quase todo o JavaScript criado é executado.
A linha de execução principal só pode processar uma tarefa por vez. Qualquer tarefa que leve mais de 50 milissegundos conta como uma tarefa longa. Se o usuário tentar interagir com a página durante uma tarefa longa ou uma atualização de renderização, o navegador precisará aguardar para processar essa interação, o que causa latência.
Para evitar isso, divida cada tarefa longa em tarefas menores, que levam menos tempo para serem executadas. Isso é chamado de dividir tarefas longas.
Dividir tarefas oferece ao navegador mais oportunidades de responder a trabalhos de maior prioridade, incluindo interações do usuário, entre outras tarefas. Isso permite que as interações aconteçam muito mais rápido, em que um usuário poderia ter notado um atraso enquanto o navegador aguardava a conclusão de uma tarefa longa.
Estratégias de gerenciamento de tarefas
O JavaScript trata cada função como uma única tarefa, porque usa um modelo de execução até a conclusão (link em inglês) de execução de tarefa. Isso significa que uma função que chama várias outras, como o exemplo a seguir, precisa ser executada até que todas as funções chamadas sejam concluídas, o que torna o navegador mais lento:
function saveSettings () { //This is a long task.
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
Se o código tiver funções que chamam vários métodos, divida-o em várias funções. Isso não apenas dá ao navegador mais oportunidades de responder à interação, mas também facilita a leitura, a manutenção e a criação de testes no seu código. Nas seções a seguir, mostramos algumas estratégias para dividir funções longas e priorizar as tarefas que as compõem.
Adiar manualmente a execução do código
É possível adiar a execução de algumas tarefas transmitindo a função relevante para
setTimeout()
. Isso funciona mesmo se você especificar um tempo limite de 0
.
function saveSettings () {
// Do critical work that is user-visible:
validateForm();
showSpinner();
updateUI();
// Defer work that isn't user-visible to a separate task:
setTimeout(() => {
saveToDatabase();
sendAnalytics();
}, 0);
}
Isso funciona melhor para uma série de funções que precisam ser executadas em ordem. O código
organizado de maneira diferente precisa de uma abordagem diferente. O próximo exemplo é uma função que processa uma grande quantidade de dados usando um loop. Quanto maior o
conjunto de dados, mais tempo leva. Não há necessariamente um bom lugar no
loop para colocar um setTimeout()
:
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
Felizmente, existem algumas outras APIs que permitem adiar a execução do código para uma
tarefa posterior. Recomendamos usar postMessage()
para tempos limite mais rápidos.
Também é possível dividir o trabalho usando requestIdleCallback()
, mas ele programa tarefas
com a prioridade mais baixa e apenas durante o tempo de inatividade do navegador. Isso significa que, se a
linha de execução principal estiver especialmente ocupada, as tarefas programadas com requestIdleCallback()
talvez nunca sejam executadas.
Usar async
/await
para criar pontos de rendimento
Para garantir que tarefas importantes voltadas ao usuário aconteçam antes de tarefas de menor prioridade, produza a linha de execução principal interrompendo brevemente a fila de tarefas para dar ao navegador oportunidades de executar tarefas mais importantes.
A maneira mais clara de fazer isso envolve um Promise
que é resolvido com uma chamada para
setTimeout()
:
function yieldToMain () {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
Na função saveSettings()
, é possível ceder à linha de execução principal após cada
etapa se você await
(preparar) a função yieldToMain()
após cada chamada de função. Isso
divide sua tarefa longa em várias tarefas:
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread:
await yieldToMain();
}
}
Importante: você não precisa produzir após cada chamada de função. Por exemplo, se você executar duas funções que resultam em atualizações críticas na interface do usuário, provavelmente não vai querer ficar entre elas. Se possível, permita que esse trabalho seja executado primeiro e, depois, considere alternar entre funções que fazem trabalho em segundo plano ou menos crítico que o usuário não vê.
Uma API de programador dedicada
As APIs mencionadas até agora podem ajudar a dividir tarefas, mas elas têm uma desvantagem significativa: quando você cede à linha de execução principal adiando o código para ser executado em uma tarefa posterior, esse código é adicionado ao final da fila de tarefas.
Se você controla todo o código na página, pode criar seu próprio agendador para priorizar tarefas. No entanto, scripts de terceiros não usarão seu agendador, então não é possível priorizar o trabalho nesse caso. Só é possível dividir ou ceder às interações do usuário.
A API do programador oferece a função postTask()
, que permite
uma programação mais refinada de tarefas e pode ajudar o navegador a priorizar o trabalho para
que as tarefas de baixa prioridade resultem na linha de execução principal. postTask()
usa promessas
e aceita uma configuração priority
.
A API postTask()
tem três prioridades disponíveis:
'background'
para as tarefas de prioridade mais baixa.'user-visible'
para tarefas de prioridade média. Esse será o padrão se nenhumpriority
estiver definido.'user-blocking'
para tarefas críticas que precisam ser executadas em alta prioridade.
O código de exemplo abaixo usa a API postTask()
para executar três tarefas com a
prioridade mais alta possível e as duas tarefas restantes com a prioridade
mais baixa possível:
function saveSettings () {
// Validate the form at high priority
scheduler.postTask(validateForm, {priority: 'user-blocking'});
// Show the spinner at high priority:
scheduler.postTask(showSpinner, {priority: 'user-blocking'});
// Update the database in the background:
scheduler.postTask(saveToDatabase, {priority: 'background'});
// Update the user interface at high priority:
scheduler.postTask(updateUI, {priority: 'user-blocking'});
// Send analytics data in the background:
scheduler.postTask(sendAnalytics, {priority: 'background'});
};
Aqui, a prioridade das tarefas é agendada para que tarefas priorizadas pelo navegador, como interações do usuário, possam aparecer.
Também é possível instanciar diferentes objetos TaskController
que compartilham
prioridades entre as tarefas, incluindo a capacidade de alterar as prioridades para
diferentes instâncias de TaskController
, conforme necessário.
Rendimento integrado com continuação usando a futura API scheduler.yield()
Importante: para uma explicação mais detalhada sobre scheduler.yield()
, leia sobre o
teste de origem
(desde a conclusão) e a explicação (links em inglês).
Uma adição proposta à API do programador é a scheduler.yield()
, uma API
projetada especificamente para resultar na linha de execução principal no navegador. O uso é semelhante à função yieldToMain()
demonstrada anteriormente nesta página:
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread with the scheduler
// API's own yielding mechanism:
await scheduler.yield();
}
}
Esse código é bastante conhecido, mas, em vez de usar yieldToMain()
, ele usa
await scheduler.yield()
.
A vantagem de scheduler.yield()
é a continuação, o que significa que, se você
gerar no meio de um conjunto de tarefas, as outras tarefas programadas continuarão na
mesma ordem após o ponto de rendimento. Isso evita que scripts de terceiros
assumam o controle da ordem em que o código é executado.
O uso de scheduler.postTask()
com priority: 'user-blocking'
também tem uma alta
probabilidade de continuação devido à alta prioridade user-blocking
. Assim, você
pode usar isso como uma alternativa até que scheduler.yield()
se torne mais amplamente
disponível.
O uso de setTimeout()
(ou scheduler.postTask()
com priority: 'user-visible'
ou sem priority
explícito) programa a tarefa na parte de trás da fila, permitindo
que outras tarefas pendentes sejam executadas antes da continuação.
Não usar isInputPending()
Compatibilidade com navegadores
- 87
- 87
- x
- x
A API isInputPending()
oferece uma maneira de verificar se um usuário tentou interagir com uma página
e conseguir apenas se uma entrada estiver pendente.
Isso permite que o JavaScript continue se nenhuma entrada estiver pendente, em vez de produzir e terminar no fundo da fila de tarefas. Isso pode resultar em melhorias de desempenho impressionantes, conforme detalhado em Intent to Ship, para sites que, de outra forma, não retornariam à linha de execução principal.
No entanto, desde o lançamento dessa API, nosso entendimento sobre rendimento melhorou, especialmente após a introdução do INP. Não recomendamos mais o uso dessa API. Em vez disso, recomendamos o rendimento independentemente de a entrada estar pendente ou não. Essa mudança nas recomendações ocorre por uma série de motivos:
- A API pode retornar incorretamente
false
em alguns casos em que um usuário interagiu. - A entrada não é o único caso em que as tarefas devem ser produzidas. Animações e outras atualizações regulares da interface do usuário podem ser igualmente importantes para fornecer uma página da Web responsiva.
- APIs de rendimento mais abrangentes, como
scheduler.postTask()
escheduler.yield()
, foram introduzidas para resolver problemas problemáticos.
Conclusão
Gerenciar tarefas é difícil, mas isso ajuda a página a responder mais rapidamente às interações do usuário. Há várias técnicas para gerenciar e priorizar tarefas, dependendo do caso de uso. Para reiterar, estes são os principais pontos que você precisa considerar ao gerenciar tarefas:
- Se entregue à linha de execução principal para tarefas essenciais voltadas ao usuário.
- Faça testes com
scheduler.yield()
. - Priorize tarefas com o
postTask()
. - Por fim, faça o mínimo de trabalho possível nas suas funções.
Com uma ou mais dessas ferramentas, você consegue estruturar o trabalho no seu aplicativo para que ele priorize as necessidades do usuário e garanta que um trabalho menos importante ainda seja feito. Isso melhora a experiência do usuário, tornando-o mais responsivo e agradável de usar.
Agradecimentos especiais a Philip Walton pela verificação técnica deste documento.
Imagem em miniatura do Unsplash, cortesia de Amirali Mirhashemian.