Usa requestIdleCallback

Muchos sitios y apps tienen muchas secuencias de comandos para ejecutar. A menudo, tu código JavaScript debe ejecutarse lo antes posible, pero, al mismo tiempo, no quieres que esto le estorbe al usuario. Si envías datos de estadísticas cuando el usuario se desplaza por la página o si agregas elementos al DOM mientras presiona el botón, tu app web puede dejar de responder, lo que genera una mala experiencia del usuario.

Uso de requestIdleCallback para programar trabajos no esenciales

La buena noticia es que ahora hay una API que puede ayudar: requestIdleCallback. De la misma manera que adoptar requestAnimationFrame nos permitió programar animaciones correctamente y maximizar nuestras posibilidades de alcanzar las 60 fps, requestIdleCallback programará el trabajo cuando haya tiempo libre al final de un fotograma o cuando el usuario esté inactivo. Esto significa que tienes la oportunidad de hacer tu trabajo sin interferir en el del usuario. Está disponible a partir de la versión 47 de Chrome, así que puedes probarla hoy mismo con Chrome Canary. Es una función experimental y las especificaciones aún están en desarrollo, por lo que es posible que cambien en el futuro.

¿Por qué debería usar requestIdleCallback?

Programar trabajo no esencial por tu cuenta es muy difícil. Es imposible saber exactamente cuánto tiempo de fotogramas queda porque, después de que se ejecutan las devoluciones de llamada de requestAnimationFrame, hay cálculos de estilo, diseño, pintura y otros elementos internos del navegador que deben ejecutarse. Una solución propia no puede tener en cuenta ninguno de ellos. Para asegurarte de que un usuario no esté interactuando de alguna manera, también deberás adjuntar objetos de escucha a cada tipo de evento de interacción (scroll, touch, click), incluso si no los necesitas para la funcionalidad, solo para asegurarte de que el usuario no esté interactuando. Por otro lado, el navegador sabe exactamente cuánto tiempo está disponible al final del fotograma y si el usuario está interactuando. Por lo tanto, a través de requestIdleCallback, obtenemos una API que nos permite aprovechar el tiempo libre de la manera más eficiente posible.

Veamos esto con más detalle y veamos cómo podemos usarlo.

Cómo comprobar requestIdleCallback

requestIdleCallback está en sus inicios, por lo que, antes de usarlo, debes verificar que esté disponible:

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

También puedes intercalar su comportamiento, lo que requiere recurrir 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);
    }

El uso de setTimeout no es ideal porque no conoce el tiempo inactivo como lo hace requestIdleCallback, pero como llamarías a tu función directamente si requestIdleCallback no estuviera disponible, no te perjudica usar el shim de esta manera. Con el shim, si requestIdleCallback está disponible, tus llamadas se redireccionarán de forma silenciosa, lo cual es excelente.

Por ahora, sin embargo, supongamos que existe.

Cómo usar requestIdleCallback

Llamar a requestIdleCallback es muy similar a requestAnimationFrame, ya que toma una función de devolución de llamada como primer parámetro:

requestIdleCallback(myNonEssentialWork);

Cuando se llama a myNonEssentialWork, se le proporciona un objeto deadline que contiene una función que muestra un número que indica cuánto tiempo queda para tu trabajo:

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

Se puede llamar a la función timeRemaining para obtener el valor más reciente. Cuando timeRemaining() devuelve cero, puedes programar otro requestIdleCallback si aún tienes más trabajo por hacer:

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

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

Cómo garantizar que se llame a tu función

¿Qué haces si tienes mucho trabajo? Es posible que te preocupe que nunca se realice la devolución de llamada. Bueno, aunque requestIdleCallback se asemeja a requestAnimationFrame, también difiere en que toma un segundo parámetro opcional: un objeto de opciones con un tiempo de espera. Si se establece, este tiempo de espera le brinda al navegador un tiempo en milisegundos para que ejecute la devolución de llamada:

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

Si se ejecuta la devolución de llamada debido al tiempo de espera, notarás dos aspectos:

  • timeRemaining() mostrará cero.
  • La propiedad didTimeout del objeto deadline será verdadera.

Si ves que didTimeout es verdadero, lo más probable es que solo quieras ejecutar el trabajo y terminar con él:

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);
}

Debido a la posible interrupción que este tiempo de espera puede causar a los usuarios (el trabajo puede hacer que tu app no responda o se bloquee), ten cuidado con la configuración de este parámetro. Cuando sea posible, permite que el navegador decida cuándo llamar a la devolución de llamada.

Usa requestIdleCallback para enviar datos de estadísticas

Analicemos el uso de requestIdleCallback para enviar datos de estadísticas. En este caso, probablemente querríamos hacer un seguimiento de un evento, por ejemplo, presionar un menú de navegación. Sin embargo, como normalmente se animan en la pantalla, querremos evitar enviar este evento a Google Analytics de inmediato. Crearemos un array de eventos para enviar y solicitar que se envíen en algún momento en el 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();
}

Ahora, deberemos usar requestIdleCallback para procesar los eventos pendientes:

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();
    }
}

Aquí puedes ver que establecí un tiempo de espera de 2 segundos, pero este valor dependerá de tu aplicación. En el caso de los datos de estadísticas, tiene sentido que se use un tiempo de espera para garantizar que los datos se informen en un período razonable en lugar de hacerlo en algún momento en el futuro.

Por último, debemos escribir la función que ejecutará requestIdleCallback.

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 ejemplo, supongo que, si requestIdleCallback no existiera, los datos de estadísticas deberían enviarse de inmediato. Sin embargo, en una aplicación de producción, es probable que sea mejor retrasar el envío con un tiempo de espera para garantizar que no entre en conflicto con ninguna interacción y no cause interrupciones.

Cómo usar requestIdleCallback para realizar cambios en el DOM

Otra situación en la que requestIdleCallback realmente puede mejorar el rendimiento es cuando debes realizar cambios no esenciales en el DOM, como agregar elementos al final de una lista de carga diferida que crece constantemente. Veamos cómo requestIdleCallback se ajusta a un marco típico.

Un marco típico.

Es posible que el navegador esté demasiado ocupado para ejecutar devoluciones de llamada en un fotograma determinado, por lo que no debes esperar que haya ningún tiempo libre al final de un fotograma para hacer más trabajo. Eso lo diferencia de algo como setImmediate, que se ejecuta por fotograma.

Si la devolución de llamada se activa al final de la trama, se programará para que se ejecute después de que se confirme la trama actual, lo que significa que se aplicarán los cambios de estilo y, lo que es más importante, se calculará el diseño. Si realizamos cambios en el DOM dentro de la devolución de llamada inactiva, se invalidarán esos cálculos de diseño. Si hay algún tipo de lectura de diseño en el siguiente fotograma, p. ej., getBoundingClientRect, clientWidth, etc., el navegador tendrá que realizar un diseño síncrono forzado, que es un posible cuello de botella de rendimiento.

Otro motivo por el que no se activan los cambios del DOM en la devolución de llamada de inactividad es que el impacto en el tiempo de cambiar el DOM es impredecible y, por lo tanto, podríamos superar fácilmente la fecha límite que proporcionó el navegador.

La práctica recomendada es solo realizar cambios en el DOM dentro de una devolución de llamada de requestAnimationFrame, ya que el navegador la programa teniendo en cuenta ese tipo de trabajo. Esto significa que nuestro código deberá usar un fragmento de documento, que luego se puede agregar en la próxima devolución de llamada requestAnimationFrame. Si usas una biblioteca de VDOM, usarás requestIdleCallback para realizar cambios, pero aplicarás los parches del DOM en la próxima devolución de llamada de requestAnimationFrame, no en la devolución de llamada inactiva.

Con eso en mente, veamos el 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();
}

Aquí creo el elemento y uso la propiedad textContent para propagarlo, pero es probable que tu código de creación de elementos sea más complejo. Después de crear el elemento, se llama a scheduleVisualUpdateIfNeeded, lo que configurará una sola devolución de llamada requestAnimationFrame que, a su vez, adjuntará el fragmento del documento al cuerpo:

function scheduleVisualUpdateIfNeeded() {

    if (isVisualUpdateScheduled)
    return;

    isVisualUpdateScheduled = true;

    requestAnimationFrame(appendDocumentFragment);
}

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

Si todo funciona bien, ahora veremos mucho menos bloqueos cuando se agreguen elementos al DOM. ¡Exacto!

Preguntas frecuentes

  • ¿Hay un polyfill? Lamentablemente, no, pero hay una corrección de compatibilidad si quieres tener un redireccionamiento transparente a setTimeout. El motivo por el que existe esta API es porque introduce un vacío muy real en la plataforma web. Inferir una falta de actividad es difícil, pero no existen APIs de JavaScript para determinar la cantidad de tiempo libre al final de la trama, por lo que, en el mejor de los casos, debes hacer conjeturas. Las APIs como setTimeout, setInterval o setImmediate se pueden usar para programar trabajo, pero no se programan para evitar la interacción del usuario de la misma manera que requestIdleCallback.
  • ¿Qué sucede si supero la fecha límite? Si timeRemaining() muestra cero, pero decides ejecutarlo por más tiempo, puedes hacerlo sin temor a que el navegador detenga tu trabajo. Sin embargo, el navegador te da una fecha límite para tratar de garantizar una experiencia fluida para tus usuarios, por lo que, a menos que haya un buen motivo, debes cumplir con ella.
  • ¿Hay un valor máximo que mostrará timeRemaining()? Sí, actualmente es de 50 ms. Cuando se intenta mantener una aplicación responsiva, todas las respuestas a las interacciones del usuario deben mantenerse por debajo de 100 ms. En caso de que el usuario interactúe, la ventana de 50 ms, en la mayoría de los casos, debería permitir que se complete la devolución de llamada inactiva y que el navegador responda a las interacciones del usuario. Es posible que recibas varias devoluciones de llamada de inactividad programadas una tras otra (si el navegador determina que hay tiempo suficiente para ejecutarlas).
  • ¿Hay algún tipo de trabajo que no deba realizar en una requestIdleCallback? Lo ideal sería que el trabajo que realices se realice en pequeños fragmentos (microtareas) con características relativamente predecibles. Por ejemplo, cambiar el DOM en particular tendrá tiempos de ejecución impredecibles, ya que activará los cálculos de estilo, el diseño, la pintura y la composición. Por lo tanto, solo debes realizar cambios en el DOM en una devolución de llamada de requestAnimationFrame, como se sugirió anteriormente. Otro aspecto que debes tener en cuenta es resolver (o rechazar) promesas, ya que las devoluciones de llamada se ejecutarán inmediatamente después de que finalice la devolución de llamada inactiva, incluso si no queda más tiempo.
  • ¿Siempre recibiré un requestIdleCallback al final de un fotograma? No, no siempre. El navegador programará la devolución de llamada cada vez que haya tiempo libre al final de un fotograma o en períodos en los que el usuario esté inactivo. No debes esperar que se llame a la devolución de llamada por fotograma y, si necesitas que se ejecute dentro de un período determinado, debes usar el tiempo de espera.
  • ¿Puedo tener varias devoluciones de llamada de requestIdleCallback? Sí, puedes hacerlo, al igual que puedes tener varias devoluciones de llamadas de requestAnimationFrame. Sin embargo, vale la pena recordar que, si la primera devolución de llamada agota el tiempo restante durante la devolución de llamada, no habrá más tiempo para otras devoluciones de llamada. Las otras devoluciones de llamada deberán esperar hasta que el navegador esté inactivo para poder ejecutarse. Según el trabajo que intentes realizar, puede ser mejor tener una sola devolución de llamada inactiva y dividir el trabajo allí. Como alternativa, puedes usar el tiempo de espera para asegurarte de que no se agote el tiempo de las devoluciones de llamada.
  • ¿Qué sucede si configuro una nueva devolución de llamada inactiva dentro de otra? La nueva devolución de llamada de inactividad se programará para que se ejecute lo antes posible, a partir del próximo fotograma (en lugar del actual).

Modo inactivo activado.

requestIdleCallback es una excelente manera de asegurarte de poder ejecutar tu código, pero sin interferir en el usuario. Es fácil de usar y muy flexible. Sin embargo, aún estamos en una etapa inicial, y la especificación no está completamente definida, por lo que recibimos con gusto cualquier comentario que tengas.

Pruébala en Chrome Canary, pruébala en tus proyectos y cuéntanos cómo te va.