Como usar requestIdleCallback

Muitos sites e apps têm muitos scripts para executar. Muitas vezes, seu JavaScript precisa ser executado o mais rápido possível, mas, ao mesmo tempo, você não quer que ele atrapalhe o usuário. Se você enviar dados de análise quando o usuário estiver rolando a página ou anexar elementos ao DOM enquanto eles estiverem tocando no botão, seu app da Web poderá parar de responder, resultando em uma experiência ruim para o usuário.

Uso de requestIdleCallback para programar trabalhos não essenciais.

A boa notícia é que agora há uma API que pode ajudar: requestIdleCallback. Da mesma forma que a adoção de requestAnimationFrame nos permitiu programar animações corretamente e maximizar nossas chances de atingir 60 QPS, o requestIdleCallback vai programar o trabalho quando houver tempo livre no final de um frame ou quando o usuário estiver inativo. Isso significa que você pode fazer seu trabalho sem atrapalhar o usuário. Ele está disponível a partir do Chrome 47, então você pode experimentar hoje mesmo com o Chrome Canary. Esse é um recurso experimental (link em inglês) e a especificação ainda está sendo desenvolvida, então algumas coisas podem mudar no futuro.

Por que devo usar requestIdleCallback?

Programar trabalhos não essenciais por conta própria é muito difícil. É impossível descobrir exatamente quanto tempo para a renderização resta porque, após a execução de callbacks requestAnimationFrame, há cálculos de estilo, layout, pintura e outros componentes internos do navegador que precisam ser executados. Uma solução interna não pode ser responsável por nenhum desses problemas. Para ter certeza de que um usuário não está interagindo de alguma forma, também seria necessário anexar listeners a todos os tipos de evento de interação (scroll, touch, click), mesmo que você não precise deles para funcionalidade, apenas para ter certeza absoluta de que o usuário não está interagindo. O navegador, por outro lado, sabe exatamente quanto tempo está disponível no final do frame e se o usuário está interagindo. Assim, com requestIdleCallback, temos uma API que permite usar o tempo livre da maneira mais eficiente possível.

Vamos analisá-lo com mais detalhes e entender como podemos usá-lo.

Verificando requestIdleCallback

O app requestIdleCallback está começando, então, antes de usá-lo, verifique se ele está disponível:

if ('requestIdleCallback' in window) {
    // Use requestIdleCallback to schedule work.
} else {
    // Do what you’d do today.
}

Também é possível corrigir o comportamento dele, o que exige voltar para setTimeout:

window.requestIdleCallback =
    window.requestIdleCallback ||
    function (cb) {
    var start = Date.now();
    return setTimeout(function () {
        cb({
        didTimeout: false,
        timeRemaining: function () {
            return Math.max(0, 50 - (Date.now() - start));
        }
        });
    }, 1);
    }

window.cancelIdleCallback =
    window.cancelIdleCallback ||
    function (id) {
    clearTimeout(id);
    }

Usar setTimeout não é uma boa opção, porque ele não sabe sobre o tempo de inatividade como o requestIdleCallback faz. No entanto, como você chamaria a função diretamente se requestIdleCallback não estivesse disponível, não seria melhor usar paliativos dessa forma. Com o paliativo, se o requestIdleCallback estiver disponível, suas chamadas serão redirecionadas silenciosamente, o que é ótimo.

Por enquanto, vamos supor que ele exista.

Como usar requestIdleCallback

Chamar requestIdleCallback é muito semelhante a requestAnimationFrame, porque usa uma função de callback como o primeiro parâmetro:

requestIdleCallback(myNonEssentialWork);

Quando myNonEssentialWork for chamado, ele receberá um objeto deadline que contém uma função que retorna um número indicando quanto tempo resta para seu trabalho:

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0)
    doWorkIfNeeded();
}

A função timeRemaining pode ser chamada para receber o valor mais recente. Quando timeRemaining() retornar zero, você poderá programar outro requestIdleCallback se ainda tiver mais trabalho a fazer:

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0 && tasks.length > 0)
    doWorkIfNeeded();

    if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

Como garantir que sua função seja chamada

O que você faz quando está muito ocupado? Talvez você esteja preocupado porque talvez seu callback nunca seja chamado. Embora a requestIdleCallback seja parecida com a requestAnimationFrame, ela também é diferente porque precisa de um segundo parâmetro opcional: um objeto de opções com a propriedade tempo limite. Se definido, esse tempo limite dá ao navegador um tempo em milissegundos para executar o callback:

// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });

Se o callback for executado devido ao disparo do tempo limite, você vai perceber duas coisas:

  • timeRemaining() vai retornar zero.
  • A propriedade didTimeout do objeto deadline será verdadeira.

Se você perceber que didTimeout é verdadeiro, provavelmente convém apenas executar o trabalho e terminar com ele:

function myNonEssentialWork (deadline) {

    // Use any remaining time, or, if timed out, just run through the tasks.
    while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
            tasks.length > 0)
    doWorkIfNeeded();

    if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

Devido à possível interrupção, esse tempo limite pode causar aos usuários (o trabalho pode fazer com que o app pare de responder ou fique instável), seja cauteloso ao definir esse parâmetro. Sempre que possível, deixe o navegador decidir quando chamar o callback.

Como usar requestIdleCallback para enviar dados de análise

Vamos usar requestIdleCallback para enviar dados de análise. Nesse caso, provavelmente queremos acompanhar um evento como, por exemplo, tocar em um menu de navegação. No entanto, como eles normalmente são animados na tela, evite enviar esse evento ao Google Analytics imediatamente. Criaremos uma matriz de eventos para enviar e solicitar que eles sejam enviados em algum momento no futuro:

var eventsToSend = [];

function onNavOpenClick () {

    // Animate the menu.
    menu.classList.add('open');

    // Store the event for later.
    eventsToSend.push(
    {
        category: 'button',
        action: 'click',
        label: 'nav',
        value: 'open'
    });

    schedulePendingEvents();
}

Agora, precisamos usar requestIdleCallback para processar eventos pendentes:

function schedulePendingEvents() {

    // Only schedule the rIC if one has not already been set.
    if (isRequestIdleCallbackScheduled)
    return;

    isRequestIdleCallbackScheduled = true;

    if ('requestIdleCallback' in window) {
    // Wait at most two seconds before processing events.
    requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
    } else {
    processPendingAnalyticsEvents();
    }
}

Aqui você pode ver que eu defini um tempo limite de 2 segundos, mas esse valor depende do seu aplicativo. Para dados de análise, faz sentido que um tempo limite seja usado para garantir que os dados sejam informados em um período razoável em vez de apenas em algum momento no futuro.

Por fim, precisamos escrever a função que requestIdleCallback vai executar.

function processPendingAnalyticsEvents (deadline) {

    // Reset the boolean so future rICs can be set.
    isRequestIdleCallbackScheduled = false;

    // If there is no deadline, just run as long as necessary.
    // This will be the case if requestIdleCallback doesn’t exist.
    if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

    // Go for as long as there is time remaining and work to do.
    while (deadline.timeRemaining() > 0 && eventsToSend.length > 0) {
    var evt = eventsToSend.pop();

    ga('send', 'event',
        evt.category,
        evt.action,
        evt.label,
        evt.value);
    }

    // Check if there are more events still to send.
    if (eventsToSend.length > 0)
    schedulePendingEvents();
}

Neste exemplo, presumi que, se requestIdleCallback não existisse, os dados de análise deveriam ser enviados imediatamente. Em um aplicativo de produção, no entanto, provavelmente é melhor atrasar o envio com um tempo limite para garantir que ele não entre em conflito com nenhuma interação e cause instabilidade.

Como usar requestIdleCallback para fazer mudanças no DOM

Outra situação em que requestIdleCallback pode realmente ajudar no desempenho é quando você precisa fazer mudanças não essenciais no DOM, como adicionar itens ao final de uma lista de carregamento lento em constante crescimento. Vejamos como o requestIdleCallback se encaixa em um frame comum.

Um frame típico.

É possível que o navegador esteja muito ocupado para executar callbacks em um determinado frame. Por isso, não haverá tempo livre no final do frame para realizar mais alguma atividade. Isso o torna diferente de algo como setImmediate, que é executado por frame.

Se o callback for acionado no final do frame, ele será programado para ir depois do commit do frame atual. Isso significa que as mudanças de estilo foram aplicadas e, o mais importante, o layout foi calculado. Se fizermos alterações no DOM dentro do callback inativo, esses cálculos de layout serão invalidados. Se houver algum tipo de leitura de layout no frame seguinte, por exemplo, getBoundingClientRect, clientWidth etc., o navegador precisará executar um layout síncrono forçado, que é um possível gargalo de desempenho.

Outro motivo para não acionar alterações do DOM na chamada de retorno ociosa é que o impacto no tempo da alteração do DOM é imprevisível e, como tal, poderíamos facilmente ultrapassar o prazo fornecido pelo navegador.

A prática recomendada é fazer mudanças no DOM apenas dentro de um callback requestAnimationFrame, já que ele é programado pelo navegador com esse tipo de trabalho em mente. Isso significa que nosso código precisará usar um fragmento de documento, que poderá ser anexado no próximo callback requestAnimationFrame. Se você estiver usando uma biblioteca VDOM, use requestIdleCallback para fazer mudanças, mas aplique os patches do DOM no próximo callback requestAnimationFrame, não no callback inativo.

Com isso em mente, vamos dar uma olhada no código:

function processPendingElements (deadline) {

    // If there is no deadline, just run as long as necessary.
    if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

    if (!documentFragment)
    documentFragment = document.createDocumentFragment();

    // Go for as long as there is time remaining and work to do.
    while (deadline.timeRemaining() > 0 && elementsToAdd.length > 0) {

    // Create the element.
    var elToAdd = elementsToAdd.pop();
    var el = document.createElement(elToAdd.tag);
    el.textContent = elToAdd.content;

    // Add it to the fragment.
    documentFragment.appendChild(el);

    // Don't append to the document immediately, wait for the next
    // requestAnimationFrame callback.
    scheduleVisualUpdateIfNeeded();
    }

    // Check if there are more events still to send.
    if (elementsToAdd.length > 0)
    scheduleElementCreation();
}

Aqui, crio o elemento e uso a propriedade textContent para preenchê-lo, mas é provável que o código de criação do elemento esteja mais envolvido. Depois de criar o elemento, scheduleVisualUpdateIfNeeded é chamado, o que configura um único callback requestAnimationFrame que, por sua vez, anexa o fragmento do documento ao corpo:

function scheduleVisualUpdateIfNeeded() {

    if (isVisualUpdateScheduled)
    return;

    isVisualUpdateScheduled = true;

    requestAnimationFrame(appendDocumentFragment);
}

function appendDocumentFragment() {
    // Append the fragment and reset.
    document.body.appendChild(documentFragment);
    documentFragment = null;
}

Tudo bem, veremos muito menos instabilidade ao anexar itens ao DOM. Excelente!

Perguntas frequentes

  • Existe um polyfill? Infelizmente, não, mas há um paliativo se você quiser um redirecionamento transparente para setTimeout. Essa API existe porque preenche uma lacuna muito real na plataforma da Web. Inferir a falta de atividade é difícil, mas não existem APIs JavaScript para determinar a quantidade de tempo livre no fim do frame, então, na melhor das hipóteses, você precisa tentar adivinhar. APIs como setTimeout, setInterval ou setImmediate podem ser usadas para programar trabalhos, mas não são cronometradas para evitar a interação do usuário da mesma forma que o requestIdleCallback.
  • O que vai acontecer se eu ultrapassar o prazo? Se timeRemaining() retornar zero, mas você optar por executar por mais tempo, será possível fazer isso sem medo de que o navegador pare seu trabalho. No entanto, o navegador dá a você o prazo para tentar garantir uma experiência tranquila para os usuários. Por isso, a menos que haja um motivo muito bom, você sempre deve cumprir o prazo.
  • Há um valor máximo que timeRemaining() vai retornar? Sim, a duração é de 50 ms. Ao tentar manter um aplicativo responsivo, todas as respostas às interações do usuário devem ser mantidas abaixo de 100 ms. Se o usuário interagir, a janela de 50 ms deve, na maioria dos casos, permitir a conclusão do retorno de chamada ocioso e que o navegador responda às interações do usuário. Você pode receber vários callbacks inativos programados em sequência (se o navegador determinar que há tempo suficiente para executá-los).
  • Há algum tipo de trabalho que não deva fazer em um requestIdleCallback? O ideal é que o trabalho seja feito em pequenos blocos (microtarefas) com características relativamente previsíveis. Por exemplo, a alteração específica do DOM terá tempos de execução imprevisíveis, já que acionará cálculos de estilo, layout, pintura e composição. Dessa forma, só faça mudanças no DOM em um callback requestAnimationFrame, conforme sugerido acima. Outra preocupação é resolver (ou rejeitar) promessas, já que os callbacks serão executados imediatamente após a conclusão do callback inativo, mesmo que não haja mais tempo restante.
  • Sempre vou receber uma requestIdleCallback ao final de um frame? Não, nem sempre. O navegador agendará o callback sempre que houver tempo livre no fim de um frame ou em períodos em que o usuário estiver inativo. Não espere que o callback seja chamado por frame e, se você precisar que ele seja executado dentro de um determinado período, use o tempo limite.
  • Posso ter vários callbacks requestIdleCallback? Sim, e é possível ter vários callbacks requestAnimationFrame. No entanto, é importante lembrar que, se o primeiro callback usar todo o tempo restante, não haverá mais tempo para outros callbacks. Os outros callbacks terão que esperar até que o navegador esteja inativo antes de serem executados. Dependendo do trabalho que você está tentando fazer, pode ser melhor ter um único callback inativo e dividir o trabalho nele. Como alternativa, você pode usar o tempo limite para garantir que nenhum callback fique faminto por tempo.
  • O que vai acontecer se eu definir um novo callback inativo dentro de outro? O novo callback inativo será programado para ser executado o mais rápido possível, começando no next frame (e não no atual).

Inativo!

A requestIdleCallback é uma ótima maneira de garantir que você possa executar seu código sem atrapalhar o usuário. É simples de usar e muito flexível. Ainda estamos no começo e as especificações ainda não foram totalmente definidas. Por isso, qualquer feedback que você tiver é bem-vindo.

Confira no Chrome Canary, faça uma experiência com seus projetos e conte para nós como está fazendo isso!