Otimizar tarefas longas

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.

Captura de tela de uma tarefa no perfil de desempenho do DevTools do Chrome. A tarefa fica no topo de uma pilha, com um manipulador de eventos de clique, uma chamada de função e mais itens abaixo dela. Ela também inclui alguns trabalhos de renderização no lado direito.
Uma tarefa iniciada por um manipulador de eventos click, mostrada no Performance Profiler do Chrome DevTools.

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.

Uma tarefa longa no Performance Profiler do DevTools do Chrome. A parte de bloqueio da tarefa (com mais de 50 milissegundos) é marcada por faixas diagonais vermelhas.
Uma tarefa longa exibida no Performance Profiler do Chrome. Tarefas longas são indicadas por um triângulo vermelho no canto da tarefa, com a parte que bloqueia a tarefa preenchida com um padrão de listras vermelhas diagonais.

Para evitar isso, divida cada tarefa longa em tarefas menores, que levam menos tempo para serem executadas. Isso é chamado de dividir tarefas longas.

Uma única
    tarefa longa versus a mesma tarefa dividida em tarefas mais curtas. A tarefa longa
    é um retângulo grande e a tarefa dividida é composta por cinco caixas menores, cujo
    comprimento é equivalente ao comprimento da tarefa longa.
Visualização de uma única tarefa longa em comparação com a mesma tarefa dividida em cinco tarefas mais curtas.

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.

Dividir uma tarefa pode facilitar a interação do usuário. 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 fragmentada permite que o manipulador de eventos seja executado antes do que teria sido feito de outra forma.
Quando as tarefas são muito longas, o navegador não consegue responder com rapidez suficiente às interações. Dividir tarefas permite que essas interações aconteçam mais rapidamente.

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();
}
A função saveSettings mostrada no criador de perfil de desempenho do Chrome. Enquanto a função de nível superior chama outras cinco funções, todo o trabalho ocorre em uma tarefa longa que bloqueia a linha de execução principal.
Uma única função saveSettings() que chama cinco funções. O trabalho é executado como parte de uma longa tarefa monolítica.

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ê.

A mesma
    função saveSettings no criador de perfil de desempenho do Chrome, agora com rendimento.
    A tarefa agora está dividida em cinco tarefas separadas, uma para cada função.
A função saveSettings() agora executa as funções filhas como tarefas separadas.

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.

Compatibilidade com navegadores

  • 94
  • 94
  • x

Origem

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 nenhum priority 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.

A
    função saveSettings mostrada no criador de perfil de desempenho do Chrome, mas usando
    postTask. postTask divide cada função saveSettings executada e
    as prioriza para que uma interação do usuário possa ser executada sem bloqueio.
Quando saveSettings() é executado, a função programa as chamadas de função individuais usando postTask(). O trabalho essencial para o usuário é programado com alta prioridade, enquanto o trabalho que o usuário não conhece está programado para execução em segundo plano. Isso permite que as interações do usuário sejam executadas mais rapidamente, porque o trabalho é dividido e priorizado adequadamente.

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().

Três diagramas que mostram tarefas sem rendimento, com rendimento e com rendimento e continuação. Sem rendimento, há tarefas longas. Com o rendimento, há mais tarefas que são mais curtas, mas podem ser interrompidas por outras tarefas não relacionadas. Com rendimento e continuação, a ordem de execução das tarefas mais curtas é preservada.
Quando você usa scheduler.yield(), a execução da tarefa continua de onde parou, mesmo após o ponto de rendimento.

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.

Rendimento de entrada com 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() e scheduler.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.