Melhor programação de JS com isInputPending()

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

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

Carregar rápido é difícil. No momento, os sites que usam o JS para renderizar o conteúdo precisam compensar o desempenho de carregamento e a capacidade de resposta de entrada: realizar todo o trabalho necessário para a exibição de uma só vez (melhor desempenho de carregamento, pior resposta de entrada) ou dividir o trabalho em tarefas menores para permanecer responsivo (pior desempenho de carregamento, melhor capacidade de resposta de entrada).

Para eliminar a necessidade, o Facebook propôs e implementou a API isInputPending() no Chromium para melhorar a capacidade de resposta sem resultar. Com base no feedback do teste de origem, fizemos várias atualizações na API e estamos felizes em anunciar que ela agora será enviada por padrão no Chromium 87.

Compatibilidade com navegadores

Compatibilidade com navegadores

  • 87
  • 87
  • x
  • x

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

Contexto

A maior parte 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 (em especial a responsividade) poderá sofrer drasticamente se o script for executado por um longo tempo. Se a página estiver fazendo muito trabalho enquanto um evento de entrada é disparado, por exemplo, a página não 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 é carregada, ela pode executar um bit de JavaScript e, em seguida, gerar 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 qual precisa informar à página. Assim, o navegador pode voltar a executar os blocos JavaScript à medida que eles são adicionados. Isso ajuda, mas pode causar outros problemas.

Cada vez que a página retorna o controle ao navegador, leva algum tempo para que ele verifique a fila de eventos de entrada, processe os eventos e selecione o próximo bloco JavaScript. Enquanto o navegador responde aos eventos mais rapidamente, o tempo geral de carregamento da página fica mais lento. E se o rendimento for alto, a página carregará muito lentamente. Se o processamento for menor, levará mais tempo para o navegador responder aos eventos do usuário, e as pessoas ficarão frustradas. Não foi 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 tivesse uma nova abordagem de carregamento que eliminaria essa compensação frustrante. Entramos em contato com nossos amigos do Chrome sobre isso e apresentamos a proposta 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 gerar o navegador.

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

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

Agora, recebemos feedback do teste de origem e de outros membros do Grupo de trabalho de desempenho da Web do W3C e implementamos mudanças na API.

Exemplo: um programador com melhor rendimento

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

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() posteriormente em uma nova macrotarefa via 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 gerencia a execução relativamente sem interrupções. No entanto, podemos perder a programação por muito tempo por outros trabalhos que querem controlar o loop de eventos ou chegar a mais QUANTUM milissegundos de latência do evento.

Está tudo bem, mas podemos melhorar? Claro!

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, além de garantir que nosso trabalho de bloqueio de tela seja executado sem interrupções. Se não houver interesse em processar nada que não seja a entrada (por exemplo, pintura) até que o trabalho seja concluído, também podemos aumentar o comprimento da QUANTUM.

Por padrão, eventos "contínuos" não são retornados de isInputPending(). Eles incluem mousemove, pointermove e outros. Se você tem interesse nisso também, 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 a isInputPending() nas principais bibliotecas de programação usando uma lógica semelhante. Esperamos que isso faça com que os desenvolvedores que usam esses frameworks se beneficiem do isInputPending() em segundo plano sem reescritas significativas.

Rendimentos nem sempre são ruins

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 do processamento de eventos de entrada, como para executar a renderização e executar outros scripts na página.

Existem casos em que o navegador não consegue atribuir corretamente os eventos de entrada pendentes. Especificamente, a configuração de clipes e máscaras complexos para iframes de origem cruzada pode relatar falsos negativos (ou seja, isInputPending() pode retornar inesperadamente falso ao segmentar esses frames). Verifique se o seu site está gerando receita com a frequência suficiente caso ele exija interações com subframes estilizados.

Lembre-se também de outras páginas que compartilham um loop de eventos. Em plataformas como o Chrome para Android, é muito 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 capacidade de resposta das páginas em primeiro plano. Ao trabalhar em segundo plano, é possível reduzir, adiar ou aumentar o rendimento com mais frequência usando a API Page Visibility.

Recomendamos que você use o isInputPending() com cuidado. Se não houver trabalho de bloqueio de usuário a ser feito, seja gentil com os outros no loop de eventos, produzindo 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 das especificações) 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 é a primeira vez que o Facebook criou uma nova API da Web e a levou da incubação de ideias à proposta de padrões para realizar o envio em um navegador. Gostaríamos de agradecer a todos que nos ajudaram a chegar até este ponto e dar um agradecimento especial a todos do Chrome que nos ajudaram a desenvolver essa ideia e a enviá-la.

Foto principal de Will H McMahan no Unsplash (links em inglês).