Como usar requestIdleCallback

Muitos sites e apps têm muitos scripts para executar. Seu JavaScript geralmente 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 estiver tocando no botão, o app da Web poderá parar de responder, resultando em uma experiência ruim para o usuário.

Usar requestIdleCallback para programar trabalhos não essenciais.

A boa notícia é que agora há uma API que pode ajudar: requestIdleCallback. Assim como a adoção de requestAnimationFrame nos permitiu programar animações corretamente e maximizar nossas chances de atingir 60 fps, requestIdleCallback vai 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. Você pode testá-lo hoje mesmo usando o Chrome Canary. Esse é um recurso experimental, e a especificação ainda está em fluxo, então as coisas podem mudar no futuro.

Por que devo usar o requestIdleCallback?

É muito difícil programar trabalhos não essenciais. É impossível descobrir exatamente quanto tempo de frame resta porque, depois que os callbacks requestAnimationFrame são executados, há cálculos de estilo, layout, pintura e outros elementos internos do navegador que precisam ser executados. Uma solução interna não pode considerar nenhuma delas. Para ter certeza de que um usuário não está interagindo de alguma forma, você também precisa anexar listeners a todos os tipos de evento de interação (scroll, touch, click), mesmo que não precise deles 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 permite usar o tempo disponível da maneira mais eficiente possível.

Vamos analisar com mais detalhes e ver como podemos usá-lo.

Verificação de requestIdleCallback

O requestIdleCallback ainda está em fase inicial. 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 usar um shim para o comportamento, o que exige o retorno a 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 é ótimo porque ele não sabe sobre o tempo de inatividade como requestIdleCallback, mas, como você chamaria sua função diretamente se requestIdleCallback não estivesse disponível, você não fica em uma situação pior com o shim dessa maneira. Com o shim, se requestIdleCallback estiver disponível, suas chamadas serão redirecionadas silenciosamente, o que é ótimo.

Por enquanto, vamos supor que ele existe.

Como usar o requestIdleCallback

Chamar requestIdleCallback é muito semelhante a requestAnimationFrame, porque usa uma função de callback como 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 outra 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 se as coisas estiverem muito ocupadas? Talvez você esteja preocupado com a possibilidade de seu callback nunca ser chamado. Embora requestIdleCallback se pareça com requestAnimationFrame, ele também é diferente porque recebe um segundo parâmetro opcional: um objeto de opções com a propriedade timeout. Esse tempo limite, se definido, 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 notar duas coisas:

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

Se o didTimeout for verdadeiro, provavelmente você vai querer 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 que esse tempo limite pode causar aos usuários (o trabalho pode fazer com que o app não responda ou fique instável), tenha cuidado ao definir esse parâmetro. Sempre que possível, deixe o navegador decidir quando chamar o callback.

Como usar o 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, é melhor evitar o envio imediato desse evento ao Google Analytics. 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, vamos 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, defini um tempo limite de dois segundos, mas esse valor depende do seu aplicativo. Para dados de análise, faz sentido usar um tempo limite 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();
}

Para este exemplo, presumi que, se requestIdleCallback não existisse, os dados de análise seriam 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 o requestIdleCallback para fazer mudanças no DOM

Outra situação em que o requestIdleCallback pode ajudar muito na performance é quando você precisa fazer mudanças não essenciais no DOM, como adicionar itens ao final de uma lista sempre crescente e com carregamento lento. Vamos conferir como requestIdleCallback se encaixa em um frame típico.

Um frame típico.

É possível que o navegador esteja muito ocupado para executar callbacks em um determinado frame. Portanto, não espere que haja nenhum tempo livre no final de um frame para fazer mais trabalho. Isso o diferencia de algo como setImmediate, que é executado por frame.

Se o callback for disparado no final do frame, ele será programado para ser executado depois que o frame atual for confirmado, o que significa que as mudanças de estilo serão aplicadas e, o mais importante, o layout calculado. Se fizermos mudanças no DOM dentro do callback ocioso, esses cálculos de layout serão invalidados. Se houver algum tipo de leitura de layout no próximo frame, por exemplo, getBoundingClientRect, clientWidth etc., o navegador terá que executar um layout síncrono forçado, que é um possível gargalo de desempenho.

Outro motivo para não acionar mudanças no DOM no callback ocioso é que o impacto no tempo de mudança do DOM é imprevisível. Por isso, podemos 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 conferir 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 seja mais complexo. 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;
}

Se tudo correr bem, vamos notar muito menos instabilidade ao anexar itens ao DOM. Excelente!

Perguntas frequentes

  • Existe um polyfill? Infelizmente, não, mas há um shim se você quiser ter um redirecionamento transparente para setTimeout. Essa API existe porque preenche uma lacuna muito real na plataforma da Web. É difícil inferir a falta de atividade, mas não existem APIs JavaScript para determinar a quantidade de tempo livre no final do frame. Portanto, na melhor das hipóteses, você terá que fazer suposições. APIs como setTimeout, setInterval ou setImmediate podem ser usadas para programar trabalhos, mas não são programadas para evitar a interação do usuário da mesma forma que requestIdleCallback.
  • O que acontece se eu ultrapassar o prazo? Se timeRemaining() retornar zero, mas você optar por executar por mais tempo, poderá fazer isso sem medo de que o navegador pare seu trabalho. No entanto, o navegador informa o prazo para tentar garantir uma experiência tranquila para os usuários. Portanto, a menos que haja um motivo muito bom, sempre respeite o prazo.
  • Há um valor máximo que timeRemaining() vai retornar? Sim, atualmente é de 50 ms. Ao tentar manter um aplicativo responsivo, todas as respostas às interações do usuário precisam 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 inativo seja concluído e que o navegador responda às interações do usuário. Você pode receber vários callbacks ociosos programados em sequência (se o navegador determinar que há tempo suficiente para executá-los).
  • Existe algum tipo de trabalho que não devo fazer em um requestIdleCallback? O ideal é que o trabalho seja feito em pequenas partes (microtarefas) com características relativamente previsíveis. Por exemplo, a mudança do DOM em particular terá tempos de execução imprevisíveis, já que aciona cálculos de estilo, layout, pintura e composição. Portanto, só faça mudanças no DOM em um callback requestAnimationFrame, como sugerido acima. Outra coisa a que você precisa prestar atenção é a resolução (ou rejeição) de promessas, porque os callbacks são executados imediatamente após o callback ocioso terminar, mesmo que não haja mais tempo restante.
  • Sempre vou receber um requestIdleCallback no final de um frame? Não, nem sempre. O navegador vai programar o callback sempre que houver tempo livre no final de um frame ou em períodos em que o usuário estiver inativo. Não espere que o callback seja chamado por frame. Se você precisar que ele seja executado em um determinado período, use o tempo limite.
  • Posso ter vários callbacks requestIdleCallback? Sim, é possível, assim como é possível ter vários callbacks requestAnimationFrame. No entanto, é importante lembrar que, se o primeiro callback usar o tempo restante durante o callback, não haverá mais tempo para outros callbacks. Os outros callbacks vão precisar esperar até que o navegador fique ocioso novamente para serem executados. Dependendo do trabalho que você está tentando realizar, talvez seja melhor ter um único callback ocioso e dividir o trabalho nele. Como alternativa, use o tempo limite para garantir que nenhum callback fique sem tempo.
  • O que acontece se eu definir um novo callback de inatividade dentro de outro? O novo callback de inatividade será programado para ser executado assim que possível, começando pelo próximo frame, em vez do atual.

Inatividade ativada.

O requestIdleCallback é uma ótima maneira de garantir que você possa executar o código, mas sem atrapalhar o usuário. Ele é simples de usar e muito flexível. Ainda é cedo, e a especificação não está totalmente definida. Portanto, seu feedback é muito bem-vindo.

Confira no Chrome Canary, teste nos seus projetos e nos conte o que achou.