Usa requestIdleCallback

Muchos sitios y apps tienen muchas secuencias de comandos para ejecutar. Por lo general, tu JavaScript debe ejecutarse lo antes posible, pero, al mismo tiempo, no quieres que se interponga en el camino del usuario. Si envías datos de análisis cuando el usuario se desplaza por la página o agregas elementos al DOM mientras presionan el botón, tu aplicación web puede dejar de responder y dar como resultado una experiencia deficiente para el 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 los 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 con el usuario. Está disponible a partir de Chrome 47, así que puedes probarlo hoy mismo con Chrome Canary. Es una característica experimental y las especificaciones todavía están en proceso de cambio, por lo que las cosas podrían cambiar en el futuro.

¿Por qué debería usar requestIdleCallback?

Programar tú mismo el trabajo no esencial es muy difícil. Es imposible descifrar exactamente cuánto tiempo de fotogramas queda, ya que, después de que se ejecutan las devoluciones de llamada de requestAnimationFrame, se deben ejecutar cálculos de estilo, diseño, pintura y otros elementos internos del navegador. Una solución casera no puede tener en cuenta ninguno de estos. 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 que puedas estar absolutamente seguro 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.

Veámoslo con más detalle y veremos cómo podemos usarlo.

Cómo comprobar requestIdleCallback

Es un día temprano para requestIdleCallback, así que antes de usarlo debes comprobar que esté disponible:

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

También puedes corregir la compatibilidad de su comportamiento, lo que requiere volver 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 muy bueno, ya que no sabe del tiempo de inactividad como requestIdleCallback, pero como llamarías a tu función directamente si requestIdleCallback no estuviera disponible, no sería peor que compensar de esta manera. Con la corrección de compatibilidad, si requestIdleCallback está disponible, tus llamadas se redireccionarán de manera silenciosa, lo cual es genial.

Por ahora, sin embargo, supongamos que existe.

Usa requestIdleCallback

Llamar a requestIdleCallback es muy similar a requestAnimationFrame, ya que toma una función de devolución de llamada como su 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() muestre cero, podrás programar otra 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);
}

Garantizar que tu función se llame

¿Qué haces cuando hay mucha actividad? Es posible que te preocupe que nunca se llame a la devolución de llamada. Aunque requestIdleCallback se parece a requestAnimationFrame, también difiere en que toma un segundo parámetro opcional: un objeto de opciones con la propiedad a timeout. Si se establece este tiempo de espera, el navegador tendrá un tiempo (en milisegundos) durante el cual deberá ejecutar la devolución de llamada:

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

Si tu devolución de llamada se ejecuta debido al tiempo de espera activado, notarás dos cosas:

  • 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 podría hacer que tu app no responda o se bloquee), ten cuidado con la configuración de este parámetro. Si es 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 rastrear un evento, como por ejemplo, tocar un menú de navegación. Sin embargo, debido a que normalmente muestran una animación en la pantalla, evitamos 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:

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 necesitaremos 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 configuré un tiempo de espera de 2 segundos, pero este valor depende de tu aplicación. En el caso de los datos estadísticos, tiene sentido usar un tiempo de espera para garantizar que los datos se informen en un plazo razonable y no solo en un momento 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, probablemente sería mejor retrasar el envío con un tiempo de espera para garantizar que no entre en conflicto con ninguna interacción y cause bloqueos.

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 encaja realmente en un fotograma 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 se espera que haya ningún tiempo libre al final de un fotograma para realizar más trabajo. Eso lo diferencia de algo como setImmediate, que se ejecuta por fotograma.

Si se activa la devolución de llamada al final del fotograma, se programará para que se ejecute después de que se confirme el fotograma actual, lo que significa que se aplicarán los cambios de diseño 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 deberá realizar un diseño síncrono forzado, lo que podría generar un cuello de botella en el rendimiento.

Otra razón por la que no se activan los cambios del DOM en la devolución de llamada inactiva es que el impacto temporal que tiene 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 requestAnimationFrame, ya que el navegador la programa con ese tipo de trabajo en mente. 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, utilizarás requestIdleCallback para realizar cambios, pero aplicarás los parches del DOM en la próxima devolución de llamada requestAnimationFrame, no en la devolución de llamada inactiva.

Con esto en mente, echemos un vistazo al 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 esté más involucrado. 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. La razón por la que existe esta API es porque introduce un vacío muy real en la plataforma web. Inferir la falta de actividad es difícil, pero no existen APIs de JavaScript para determinar la cantidad de tiempo libre al final del fotograma, así que, en el mejor de los casos, debes hacer conjeturas. Las APIs como setTimeout, setInterval o setImmediate se pueden usar para programar un trabajo, pero no tienen un horario para evitar la interacción del usuario como lo hace requestIdleCallback.
  • ¿Qué sucede si excedo el plazo? Si timeRemaining() devuelve cero, pero optas por ejecutar el servicio durante más tiempo, puedes hacerlo sin miedo 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 menos de 100 ms. En caso de que el usuario interactúe, la ventana de 50 ms, en la mayoría de los casos, deberá 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 inactivas programadas de forma consecutiva (si el navegador determina que hay suficiente tiempo para ejecutarlas).
  • ¿Hay algún tipo de trabajo que no deba hacer en una requestIdleCallback? Lo ideal sería que el trabajo que realices se realice en pequeñas partes (microtareas) con características relativamente predecibles. Por ejemplo, si se modifica el DOM en particular, los tiempos de ejecución serán impredecibles, ya que activará cálculos de estilo, diseño, pintura y composición. Por lo tanto, solo debes realizar cambios de DOM en una devolución de llamada de requestAnimationFrame, como se sugiere más arriba. Otra cosa a la que se debe tener cuidado es resolver (o rechazar) las promesas, ya que las devoluciones de llamada se ejecutarán inmediatamente después de que finalice la devolución de llamada inactiva, aun si no queda más tiempo.
  • ¿Siempre obtendré un requestIdleCallback al final de un fotograma? No, no siempre. El navegador programará la devolución de llamada cuando haya tiempo libre al final de un fotograma o en períodos en los que el usuario esté inactivo. No deberías 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 requestIdleCallback? Sí, en la medida en que puedes tener varias devoluciones de llamada requestAnimationFrame. Sin embargo, vale la pena recordar que si tu primera devolución de llamada consume el tiempo restante durante la devolución de llamada, no habrá más tiempo para ninguna otra devolución de llamada. Las otras devoluciones de llamada deberán esperar hasta que el navegador esté inactivo antes de poder ejecutarlas. 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 ninguna devolución de llamada se pierda de tiempo.
  • ¿Qué sucede si configuro una nueva devolución de llamada inactiva dentro de otra? La nueva devolución de llamada inactiva se programará para ejecutarse lo antes posible, a partir del fotograma next (en lugar del fotograma actual).

¡Inactividad!

requestIdleCallback es una manera excelente de asegurarte de que puedes ejecutar tu código, pero sin interferir con el usuario. Es fácil de usar y muy flexible. Sin embargo, el proceso aún es temprano y las especificaciones no están completamente establecidas, por lo que todos los comentarios que tengas son bienvenidos.

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