Usa requestIdleCallback

Muchos sitios y apps tienen que ejecutar muchas secuencias de comandos. A menudo, tu JavaScript se debe ejecutar lo antes posible, pero al mismo tiempo no deseas 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 app web puede dejar de responder y la experiencia del usuario es deficiente.

Cómo usar requestIdleCallback para programar trabajos no esenciales

La buena noticia es que ahora existe una API que puede ayudar: requestIdleCallback. De la misma manera que adoptar requestAnimationFrame nos permitió programar animaciones de forma correcta 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 hay una oportunidad de hacer tu trabajo sin interponerse en el camino del usuario. Está disponible a partir de Chrome 47, así que puedes probarlo hoy mismo con Chrome Canary. Se trata de una característica experimental, y la especificación aún está en proceso de cambio, por lo que las cosas podrían cambiar en el futuro.

¿Por qué debería usar requestIdleCallback?

Es muy difícil programar el trabajo no esencial por tu cuenta. Es imposible determinar exactamente cuánto tiempo de procesamiento queda porque, después de que se ejecutan las devoluciones de llamada de requestAnimationFrame, hay que realizar cálculos de estilo, diseño, pintura y otros elementos internos del navegador. Una solución de lanzamiento al mercado no puede dar cuenta de ninguna de ellas. Para asegurarte de que un usuario no interactúe de alguna manera, también deberías 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 tener la seguridad absoluta de que el usuario no está interactuando. El navegador, por otro lado, sabe exactamente cuánto tiempo está disponible al final del fotograma y si el usuario está interactuando. Por lo tanto, mediante requestIdleCallback obtenemos una API que nos permite usar el tiempo libre de la manera más eficiente posible.

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

Buscando requestIdleCallback

Es muy temprano para requestIdleCallback, así que antes de usarlo, asegúrate de que esté disponible para su uso:

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

También puedes corregir 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 bueno porque no conoce el tiempo de inactividad como requestIdleCallback, pero como llamarías a tu función directamente si requestIdleCallback no estuviera disponible, no es peor que apliques de esta manera con la corrección de compatibilidad. Con la corrección de compatibilidad, si requestIdleCallback estuviera disponible, tus llamadas se redireccionarán silenciosamente, lo cual es genial.

Sin embargo, por ahora, 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 llame a myNonEssentialWork, se le entregará un objeto deadline que contiene una función que muestra un número que indica cuánto tiempo resta 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);
}

La garantía de que tu función se denomina

¿Qué haces si estás muy ocupado? Tal vez te preocupe que nunca se llame a tu devolución de llamada. Aunque requestIdleCallback se parece a requestAnimationFrame, también difiere en que requiere un segundo parámetro opcional: un objeto de opciones con una propiedad tiempo de espera. Este tiempo de espera, si se configura, le da al navegador un tiempo en milisegundos en el que debe 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 a la activación del 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 podría hacer que tu app deje de responder o se bloquee), ten cuidado cuando configures este parámetro. Cuando puedas, deja que el navegador decida cuándo llamar a la devolución de llamada.

Cómo usar requestIdleCallback para enviar datos de estadísticas

Revisemos los datos con requestIdleCallback para enviar datos de estadísticas. En este caso, es probable que debamos rastrear un evento como, por ejemplo, presionar un menú de navegación. Sin embargo, debido a que normalmente se muestran animaciones en la pantalla, debemos evitar enviar este evento a Google Analytics de inmediato. Crearemos un array de eventos para enviar y solicitaremos 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 establecí 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 período razonable y no solo 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, supuse que si requestIdleCallback no existiera, los datos de estadísticas deberían enviarse de inmediato. Sin embargo, en una aplicación de producción, 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 puede mejorar mucho el rendimiento es cuando debes realizar cambios de DOM no esenciales, como agregar elementos al final de una lista de carga diferida que crece cada día. Veamos cómo requestIdleCallback en realidad 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 realizar más trabajos. Eso lo diferencia de algo como setImmediate, que se ejecuta por fotograma.

Si la devolución de llamada se activa al final del fotograma, se programará para que se ejecute una vez que se confirme el fotograma actual, lo que significa que se habrán aplicado los cambios de estilo y, sobre todo, se calculará el diseño. Si hacemos 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 sincrónico forzado, que es un posible cuello de botella de rendimiento.

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

La práctica recomendada es realizar cambios en el DOM solo dentro de una devolución de llamada requestAnimationFrame, ya que el navegador lo programa con ese tipo de trabajo en mente. Eso significa que nuestro código deberá usar un fragmento de documento, que luego se podrá agregar en la próxima devolución de llamada requestAnimationFrame. Si usas una biblioteca VDOM, deberías usar requestIdleCallback para realizar cambios, pero aplicarás los parches del DOM en la próxima devolución de llamada requestAnimationFrame, no 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 completarlo, pero es probable que el código de creación de tu elemento sea más complejo. Después de crear el elemento scheduleVisualUpdateIfNeeded, se 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 está bien, ahora veremos muchos 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. Esta API existe porque conecta una brecha muy real en la plataforma web. Inferir la falta de actividad es difícil, pero no existen APIs de JavaScript para determinar el tiempo libre al final del fotograma, por lo que, en el mejor de los casos, debes hacer suposiciones. Las APIs como setTimeout, setInterval o setImmediate se pueden usar para programar trabajos, pero no están programadas para evitar la interacción del usuario de la manera en que requestIdleCallback.
  • ¿Qué sucede si excedí el plazo? Si timeRemaining() muestra cero, pero decides que se ejecute durante más tiempo, puedes hacerlo sin temor que el navegador detenga tu trabajo. Sin embargo, el navegador te da una fecha límite para intentar garantizar una experiencia fluida para tus usuarios, así que, a menos que haya un muy buen motivo, siempre debes cumplir con el plazo.
  • ¿Existe un valor máximo que mostrará timeRemaining()? Sí, en este momento, es de 50 ms. Si intentas mantener una aplicación responsiva, todas las respuestas a las interacciones del usuario deben mantenerse por debajo de los 100 ms. Si el usuario interactúa, la ventana de 50 ms debe, en la mayoría de los casos, 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 llamadas inactivas programadas unas con otras (si el navegador determina que hay tiempo suficiente para ejecutarlas).
  • ¿Hay algún tipo de trabajo que no debería hacer en requestIdleCallback? Idealmente, el trabajo que hagas debería ser en fragmentos pequeños (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 requestAnimationFrame, como se sugiere más arriba. Otro aspecto a tener en cuenta es la resolución (o el rechazo) de las 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 obtendré un elemento 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 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 aprovechar el tiempo de espera.
  • ¿Puedo tener varias devoluciones de llamada de requestIdleCallback? Sí, en la medida de lo posible, con varias devoluciones de llamada de requestAnimationFrame. Sin embargo, debes recordar que si tu 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 aprovechar el tiempo de espera para asegurarte de que ninguna devolución de llamada se pierda tiempo.
  • ¿Qué sucede si configuro una nueva devolución de llamada inactiva dentro de otra? Se programará la nueva devolución de llamada inactiva para que se ejecute lo antes posible a partir del fotograma siguiente (en lugar del actual).

Permanece inactivo

requestIdleCallback es una excelente manera de asegurarte de que puedes ejecutar tu código, pero sin interponerse en el camino del usuario. Es fácil de usar y muy flexible. Aún es muy pronto y las especificaciones no están totalmente establecidas, por lo que aceptamos cualquier comentario que tengas.

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