Como usar requestIdleCallback

Muitos sites e apps têm muitos scripts para executar. Com frequência, o 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 ele toca no botão, seu app da Web pode parar de responder, resultando em uma experiência ruim para o usuário.

Uso de requestIdleCallback para agendar trabalhos não essenciais.

A boa notícia é que agora existe uma API que pode ajudar: requestIdleCallback. Da mesma forma que adotar requestAnimationFrame nos permitiu programar animações corretamente e maximizar nossas chances de atingir 60 QPS, o requestIdleCallback programará o trabalho quando houver tempo livre no final de um frame ou quando o usuário estiver inativo. Isso significa que há uma oportunidade de fazer seu trabalho sem atrapalhar o usuário. Ele está disponível a partir do Chrome 47, então você pode começar a usar hoje mesmo usando o Chrome Canary. É um recurso experimental, e a especificação ainda está em fluxo, então as 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 de renderização de frames resta porque, após a execução dos callbacks de requestAnimationFrame, há cálculos de estilo, layout, pintura e outros componentes internos do navegador que precisam ser executados. Uma solução caseira não pode explicar nenhum desses problemas. Para garantir que um usuário não esteja interagindo de alguma forma, você também vai precisar anexar listeners a todos os tipos de evento de interação (scroll, touch, click), mesmo que eles não sejam necessários para a 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 nos permite usar qualquer tempo livre da maneira mais eficiente possível.

Vamos analisar isso com mais detalhes e descobrir como usá-los.

Verificando requestIdleCallback

requestIdleCallback está no começo. Por isso, antes de usar, verifique se ele está disponível:

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

Você também pode 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);
    }

O uso de setTimeout não é ideal porque ele não sabe sobre o tempo de inatividade como o requestIdleCallback sabe. No entanto, como você chamaria a função diretamente se requestIdleCallback não estivesse disponível, não seria pior trabalhar dessa forma. Com o paliativo, caso requestIdleCallback esteja disponível, suas chamadas serão redirecionadas silenciosamente, o que é ótimo.

Por enquanto, porém, vamos presumir que ele existe.

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 é chamado, ele recebe um objeto deadline, que contém uma função que retorna um número indicando quanto tempo resta para o 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 a função seja chamada

O que você faz se as coisas estão realmente lotadas? Talvez você se preocupe que seu callback nunca seja chamado. Embora requestIdleCallback seja semelhante a requestAnimationFrame, ele também difere porque usa um segundo parâmetro opcional: um objeto de opções com uma propriedade de tempo limite. Esse tempo limite, se definido, dá ao navegador um tempo em milissegundos para executar o retorno de chamada:

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

Se o callback for executado devido ao acionamento do tempo limite, você notará duas coisas:

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

Se você perceber que o didTimeout é verdadeiro, recomendamos executar o trabalho e terminar:

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 seu app não responda ou instabilidade). Tenha cuidado ao definir esse parâmetro. Quando possível, deixe que o navegador decida quando chamar o callback.

Usar requestIdleCallback para enviar dados de análise

Vamos conferir como 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, não convém enviar esse evento ao Google Analytics imediatamente. Vamos criar 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 todos os 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 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 de tempo razoável, e não apenas em algum momento no futuro.

Por fim, precisamos criar 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. No entanto, em um aplicativo de produção, é 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 melhorar o desempenho é quando você tem mudanças não essenciais do DOM para fazer, como adicionar itens ao final de uma lista de carregamento lento que está em constante crescimento. Vejamos como requestIdleCallback se encaixa em um frame comum.

Um frame típico.

O navegador pode ficar muito ocupado para executar callbacks em um determinado frame. Por isso, não haverá tempo livre no final de um frame para realizar mais trabalhos. 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 continuar após o commit do frame atual, o que significa que alterações de estilo serão aplicadas e, o mais importante, o layout será calculado. Se fizermos mudanças no DOM dentro do callback inativo, esses cálculos de layout serão invalidados. Se houver qualquer tipo de leitura de layout no próximo frame, como getBoundingClientRect, clientWidth etc., o navegador vai 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 no callback inativo é que o impacto temporal da alteração do DOM é imprevisível e, assim, podemos facilmente ultrapassar o prazo fornecido pelo navegador.

A prática recomendada é fazer mudanças no DOM somente 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 pode ser anexado ao próximo callback requestAnimationFrame. Se você estiver usando uma biblioteca VDOM, utilize 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 analisar o 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, a chamada é feita, o que configura um único callback requestAnimationFrame que, por sua vez, anexa o fragmento de 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, agora vamos observar muito menos instabilidade ao anexar itens ao DOM. Excelente!

Perguntas frequentes

  • Existe um polyfill? Infelizmente, não, mas há um paliativo caso você queira ter um redirecionamento transparente para setTimeout. Essa API existe porque preenche uma lacuna muito real na plataforma Web. Inferir uma falta de atividade é difícil, mas não existem APIs JavaScript para determinar a quantidade de tempo livre no final do frame, portanto, na melhor das hipóteses, é preciso adivinhar. APIs como setTimeout, setInterval ou setImmediate podem ser usadas para programar o trabalho, 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 exceder o prazo? Se timeRemaining() retornar zero, mas você optar por executar por mais tempo, é possível fazer isso sem medo de que o navegador interrompa seu trabalho. No entanto, o navegador lhe dá o prazo para tentar garantir uma experiência tranquila para seus usuários. Portanto, a menos que haja um bom motivo, você deve sempre cumprir o prazo.
  • Há um valor máximo que timeRemaining() vai retornar? Sim, o momento é 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 que o callback ocioso seja concluído e que o navegador responda às interações do usuário. Você pode receber vários callbacks de inatividade programados em sequência (se o navegador determinar que há tempo suficiente para executá-los).
  • Há algum tipo de trabalho que eu não deva fazer em um requestIdleCallback? O ideal é que seu trabalho seja feito em pequenos blocos (microtarefas) com características relativamente previsíveis. Por exemplo, a alteração do DOM em particular terá tempos de execução imprevisíveis, pois acionará cálculos de estilo, layout, pintura e composição. Portanto, só faça mudanças no DOM em um callback requestAnimationFrame, conforme sugerido acima. Outra coisa a se preocupar é resolver (ou rejeitar) promessas, já que os callbacks serão executados imediatamente após a conclusão do callback ocioso, mesmo que não haja mais tempo restante.
  • Sempre haverá um requestIdleCallback no final de um frame? Não, nem sempre. O navegador programará o callback sempre que houver tempo livre no final de um frame ou em períodos em que o usuário estiver inativo. O callback não deve ser chamado por frame e, se você precisar que ele seja executado em um determinado período, use o tempo limite.
  • Posso ter vários callbacks requestIdleCallback? Sim, na medida do possível, desde que você tenha vários callbacks de requestAnimationFrame. No entanto, é importante lembrar que, se o primeiro callback usar o tempo restante durante o callback, não haverá mais tempo restante para outros callbacks. Os outros callbacks terão que esperar até o navegador ficar inativo antes de poderem ser executados. Dependendo do trabalho que você está tentando fazer, pode ser melhor ter um único callback ocioso e dividir o trabalho nele. Como alternativa, você pode usar o tempo limite para garantir que nenhum retorno de chamada fique sem tempo.
  • O que vai acontecer se eu definir um novo callback inativo dentro de outro? O novo callback inativo será programado para execução o mais rápido possível, começando pelo próximo frame, e não pelo atual.

Inativo!

requestIdleCallback é uma ótima maneira de garantir que você possa executar seu código, mas sem atrapalhar o usuário. É simples de usar e muito flexível. No entanto, ainda estamos no início, e as especificações não estão completamente definidas, portanto, qualquer feedback que você tiver é bem-vindo.

Confira no Chrome Canary, teste seus projetos e nos conte como você se saiu!