Melhor programação de JS com isInputPending()

Uma nova API JavaScript que pode ajudar a evitar a compensação entre desempenho de carregamento e capacidade de resposta de entrada.

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

O carregamento rápido é difícil. Os sites que usam JS para renderizar o conteúdo atualmente precisam fazer um trade-off entre a performance de carregamento e a capacidade de resposta de entrada: ou executam todo o trabalho necessário para exibição de uma só vez (melhor desempenho de carregamento, pior capacidade de resposta de entrada) ou dividem o trabalho em tarefas menores para permanecerem responsivos à entrada e pintura (pior desempenho de carregamento, melhor capacidade de resposta de entrada).

Para eliminar a necessidade de fazer esse trade-off, o Facebook propôs e implementou a API isInputPending() no Chromium para melhorar a capacidade de resposta sem renderizar. Com base no feedback do teste de origem, fizemos várias atualizações na API e temos o prazer de anunciar que ela agora é enviada por padrão no Chromium 87.

Compatibilidade com navegadores

Compatibilidade com navegadores

  • Chrome: 87.
  • Edge: 87.
  • Firefox: não é compatível.
  • Safari: não é compatível.

Origem

O isInputPending() foi enviado em navegadores baseados no Chromium a partir da versão 87. Nenhum outro navegador sinalizou a intenção de enviar a API.

Contexto

A maioria do trabalho no ecossistema JS atual é feita em uma única linha de execução: a principal. Isso fornece um modelo de execução robusto para os desenvolvedores, mas a experiência do usuário (principalmente a capacidade de resposta) pode ser afetada drasticamente se o script for executado por um longo tempo. Se a página estiver fazendo muito trabalho enquanto um evento de entrada é acionado, por exemplo, ela não vai processar o evento de entrada de clique até que esse trabalho seja concluído.

A prática recomendada atual é lidar com esse problema dividindo o JavaScript em blocos menores. Enquanto a página está sendo carregada, ela pode executar um pouco de JavaScript e, em seguida, ceder e transmitir o controle de volta ao navegador. O navegador pode então verificar a fila de eventos de entrada e ver se há algo sobre o que ele precisa informar à página. Em seguida, o navegador pode voltar a executar os blocos JavaScript conforme eles são adicionados. Isso ajuda, mas pode causar outros problemas.

Cada vez que a página passa o controle de volta ao navegador, leva algum tempo para que ele verifique a fila de eventos de entrada, processe eventos e selecione o próximo bloco de JavaScript. Embora o navegador responda aos eventos mais rapidamente, o tempo de carregamento geral da página fica mais lento. Se ela for muito usada, a página vai carregar muito lentamente. Se a redução da frequência diminuir, o navegador levará mais tempo para responder aos eventos do usuário, e as pessoas ficarão frustradas. Não é divertido.

Um diagrama mostrando que, quando você executa tarefas longas de JS, o navegador tem menos tempo para despachar eventos.

No Facebook, queríamos saber como seria se criássemos uma nova abordagem de carregamento que eliminasse esse trade-off frustrante. Entramos em contato com nossos amigos do Chrome e criamos a proposta para isInputPending(). A API isInputPending() é a primeira a usar o conceito de interrupções para entradas do usuário na Web e permite que o JavaScript verifique a entrada sem ceder ao navegador.

Um diagrama mostrando que isInputPending() permite que o JS verifique se há entradas pendentes do usuário, sem retornar completamente a execução ao navegador.

Como houve interesse na API, fizemos parceria com nossos colegas do Chrome para implementar e enviar o recurso no Chromium. Com a ajuda dos engenheiros do Chrome, os patches foram lançados após um teste de origem, que é uma forma de o Chrome testar mudanças e receber feedback dos desenvolvedores antes de lançar uma API.

Agora, coletamos o feedback do teste de origem e dos outros membros do grupo de trabalho de desempenho da Web do W3C e implementamos mudanças na API.

Exemplo: um programador de rendimento

Suponha que você tenha um monte de trabalho de bloqueio de exibição para carregar sua página, por exemplo, gerando marcação de componentes, fatorando números primos ou apenas exibindo um ícone de carregamento legal. Cada um deles é dividido em um item de trabalho separado. Usando o padrão do programador, vamos esboçar como processar nosso trabalho em uma função hipotética processWorkQueue():

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (performance.now() >= DEADLINE) {
    // Yield the event loop if we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

Invocando processWorkQueue() posteriormente em uma nova macrotarefa por setTimeout(), oferecemos ao navegador a capacidade de permanecer um pouco responsivo à entrada (ele pode executar manipuladores de eventos antes que o trabalho seja retomado), enquanto ainda consegue uma execução relativamente sem interrupções. No entanto, podemos perder a programação por muito tempo por outro trabalho que queira controlar o loop de eventos ou chegar a até QUANTUM milissegundos extra de latência de evento.

Isso está bom, mas podemos melhorar? Sim!

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending() || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event, or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

Ao introduzir uma chamada para navigator.scheduling.isInputPending(), podemos responder à entrada mais rapidamente, garantindo que nosso trabalho de bloqueio de exibição seja executado sem interrupções. Se não quisermos processar nada além da entrada (por exemplo, pintura) até que o trabalho seja concluído, também podemos aumentar o comprimento de QUANTUM.

Por padrão, os eventos "contínuos" não são retornados de isInputPending(). Eles incluem mousemove, pointermove e outros. Se você também tiver interesse em ceder o acesso a esses recursos, não tem problema. Ao fornecer um objeto para isInputPending() com includeContinuous definido como true, podemos começar:

const DEADLINE = performance.now() + QUANTUM;
const options = { includeContinuous: true };
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending(options) || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event (any of them!), or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

Pronto! Frameworks como o React estão criando suporte para isInputPending() nas bibliotecas de programação principais usando uma lógica semelhante. Esperamos que isso leve os desenvolvedores que usam esses frameworks a se beneficiarem de isInputPending() nos bastidores sem reescritas significativas.

Rendimento nem sempre é ruim

Vale ressaltar que produzir menos não é a solução certa para todos os casos de uso. Há muitos motivos para retornar o controle ao navegador, além de processar eventos de entrada, como renderizar e executar outros scripts na página.

Há casos em que o navegador não consegue atribuir corretamente eventos de entrada pendentes. Em particular, definir clipes e máscaras complexos para iframes de origem cruzada pode gerar falsos negativos. Ou seja, isInputPending() pode retornar false inesperadamente ao segmentar esses frames. Certifique-se de que você está gerando com frequência suficiente se o site exigir interações com subframes estilizados.

Esteja atento a outras páginas que também compartilham um loop de eventos. Em plataformas como o Chrome para Android, é bastante comum que várias origens compartilhem um loop de eventos. isInputPending() nunca vai retornar true se a entrada for enviada para um frame de origem cruzada. Portanto, as páginas em segundo plano podem interferir na responsividade das páginas em primeiro plano. Talvez você queira reduzir, adiar ou ceder com mais frequência ao trabalhar em segundo plano usando a API Page Visibility.

Recomendamos que você use o isInputPending() com cautela. Se não houver trabalho de bloqueio do usuário a ser feito, seja gentil com os outros no loop de eventos retornando com mais frequência. Tarefas longas podem ser prejudiciais.

Feedback

  • Deixe feedback sobre a especificação no repositório is-input-pending.
  • Entre em contato com @acomminos (um dos autores da especificação) no Twitter.

Conclusão

Estamos felizes com o lançamento do isInputPending() e que os desenvolvedores possam começar a usá-lo hoje mesmo. Essa API é a primeira vez que o Facebook criou uma nova API da Web e a levou da incubação da ideia à proposta de padrões para o envio em um navegador. Gostaríamos de agradecer a todos que nos ajudaram a chegar a este ponto e dar um destaque especial a todos no Chrome que nos ajudaram a dar vida a essa ideia e fazer o lançamento!

Foto principal de Will H McMahan no Unsplash.