Melhor programação de JS com isInputPending()

Uma nova API JavaScript que pode ajudar a evitar o equilíbrio entre a performance de carregamento e a capacidade de resposta de entrada.

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

É difícil carregar rapidamente. 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 permanecer responsivo à 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 responsividade) 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 verificar a fila de eventos de entrada e conferir se há algo 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 devolve o controle 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. E se fizermos isso com muita frequência, a página vai carregar muito lentamente. Se fizermos isso com menos frequência, o navegador vai demorar mais para responder aos eventos do usuário, e as pessoas vão ficar frustradas. Não é divertido.

Um diagrama mostrando que, quando você executa tarefas longas de JS, o navegador tem menos tempo para enviar 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á entrada do usuário pendente, sem retornar 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 discreto. 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();
}

Ao invocar processWorkQueue() mais tarde em uma nova macrotarefa usando setTimeout(), permitimos que o navegador continue um pouco responsivo à entrada (ele pode executar manipuladores de eventos antes que o trabalho seja retomado) e ainda consiga ser executado de forma ininterrupta. No entanto, podemos ter a programação cancelada por um longo período por outro trabalho que queira controlar o loop de eventos ou ter até QUANTUM milissegundos extras 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 houver interesse em processar nada além da entrada (por exemplo, pintura) até que o trabalho seja concluído, também é possível aumentar a duração 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 quiser 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 beneficiar do isInputPending() nos bastidores sem reescritas significativas.

Ceder não é sempre ruim

Vale a pena notar 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.

Também é preciso ter cuidado com outras páginas que 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 isInputPending() com discrição. 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 com a possibilidade de os desenvolvedores começarem a usá-lo hoje. Essa é a primeira vez que o Facebook cria uma nova API da Web e a leva da incubação de ideias à proposta de padrões para ser lançada 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 a enviar.

Foto principal de Will H McMahan no Unsplash.