Optimiza las tareas largas

Los consejos comunes para acelerar tus apps de JavaScript suelen incluir los siguientes consejos: "No bloquees el subproceso principal" y "Divide tus tareas largas". En esta página, se desglosa lo que significa ese consejo y por qué es importante la optimización de tareas en JavaScript.

¿Qué es una tarea?

Una tarea es cualquier tarea discreta que realiza el navegador. Esto incluye el procesamiento, el análisis de HTML y CSS, la ejecución del código JavaScript que escribes y otras funciones sobre las que quizás no tengas control directo. El código JavaScript de tus páginas es una fuente importante de tareas del navegador.

Captura de pantalla de una tarea en el perfil de rendimiento de Herramientas para desarrolladores de Chrome. La tarea se encuentra en la parte superior de una pila, con un controlador de eventos de clic, una llamada a función y más elementos debajo. La tarea también incluye trabajo de renderización en el lado derecho.
Una tarea iniciada por un controlador de eventos click en el generador de perfiles de rendimiento de las Herramientas para desarrolladores de Chrome.

Las tareas afectan el rendimiento de varias maneras. Por ejemplo, cuando un navegador descarga un archivo JavaScript durante el inicio, pone en cola las tareas para analizar y compilar ese JavaScript, de modo que pueda ejecutarse. Más adelante en el ciclo de vida de la página, otras tareas comienzan cuando tu JavaScript funciona, como impulsar interacciones con controladores de eventos, animaciones basadas en JavaScript y actividades en segundo plano, como la recopilación de estadísticas. Todo esto, a excepción de los trabajadores web y otras APIs similares, ocurre en el subproceso principal.

¿Cuál es el subproceso principal?

El subproceso principal es donde se ejecutan la mayoría de las tareas en el navegador y donde se ejecuta casi todo el código JavaScript que escribes.

El subproceso principal solo puede procesar una tarea a la vez. Cualquier tarea que tarde más de 50 milisegundos se considera una tarea larga. Si el usuario intenta interactuar con la página durante una tarea larga o una actualización de renderización, el navegador debe esperar para controlar esa interacción, lo que causa latencia.

Es una tarea extensa en el generador de perfiles de rendimiento de las Herramientas para desarrolladores de Chrome. La parte de bloqueo de la tarea (superior a 50 milisegundos) está marcada con franjas diagonales rojas.
Es una tarea larga que se muestra en el generador de perfiles de rendimiento de Chrome. Las tareas largas se indican con un triángulo rojo en la esquina de la tarea, con la parte de bloqueo completada con un patrón de franjas rojas diagonales.

Para evitarlo, divide cada tarea larga en tareas más pequeñas que tarden menos en ejecutarse. Esto se llama dividir las tareas largas.

Una sola tarea larga en comparación con la misma tarea dividida en tareas más cortas. La tarea larga es un rectángulo grande y la tarea fragmentada consta de cinco cajas más pequeñas cuya longitud se suma a la longitud de la tarea larga.
Visualización de una sola tarea larga en comparación con esa misma tarea dividida en cinco tareas más cortas.

Dividir las tareas le brinda al navegador más oportunidades de responder a trabajos de mayor prioridad, incluidas las interacciones del usuario, entre otras tareas. Esto permite que las interacciones ocurran mucho más rápido, que, de lo contrario, podría haber notado un retraso mientras el navegador esperaba que finalice una tarea larga.

Dividir una tarea puede facilitar la interacción del usuario. En la parte superior, una tarea larga bloquea la ejecución de un controlador de eventos hasta que la tarea finalice. En la parte inferior, la tarea fragmentada permite que el controlador de eventos se ejecute antes de lo que lo haría de otro modo.
Cuando las tareas son demasiado largas, el navegador no puede responder con la rapidez suficiente a las interacciones. Dividir las tareas permite que esas interacciones ocurran más rápido.

Estrategias de administración de tareas

JavaScript trata cada función como una sola tarea, ya que usa un modelo de ejecución de ejecución a finalización de la tarea. Esto significa que una función que llama a muchas otras funciones, como el siguiente ejemplo, debe ejecutarse hasta que se completen todas las funciones llamadas, lo que ralentiza el navegador:

function saveSettings () { //This is a long task.
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}
La función saveSettings que se muestra en el generador de perfiles de rendimiento de Chrome. Si bien la función de nivel superior llama a otras cinco funciones, todo el trabajo se lleva a cabo en una tarea larga que bloquea el subproceso principal.
Una sola función saveSettings() que llama a cinco funciones. El trabajo se ejecuta como parte de una tarea monolítica larga.

Si tu código contiene funciones que llaman a varios métodos, divídelo en varias funciones. Esto no solo le brinda al navegador más oportunidades de responder a la interacción, sino que también facilita la lectura, el mantenimiento y la escritura de pruebas de tu código. En las siguientes secciones, se explican algunas estrategias para dividir las funciones largas y priorizar las tareas que las componen.

Aplaza la ejecución del código de forma manual

Para posponer la ejecución de algunas tareas, pasa la función correspondiente a setTimeout(). Esto funciona incluso si especificas un tiempo de espera de 0.

function saveSettings () {
  // Do critical work that is user-visible:
  validateForm();
  showSpinner();
  updateUI();

  // Defer work that isn't user-visible to a separate task:
  setTimeout(() => {
    saveToDatabase();
    sendAnalytics();
  }, 0);
}

Esto funciona mejor para una serie de funciones que necesitan ejecutarse en orden. El código que se organiza de forma diferente necesita un enfoque distinto. El siguiente ejemplo es una función que procesa una gran cantidad de datos con un bucle. Cuanto más grande sea el conjunto de datos, más tiempo tardará este proceso y no hay necesariamente un buen lugar en el bucle para colocar un setTimeout():

function processData () {
  for (const item of largeDataArray) {
    // Process the individual item here.
  }
}

Afortunadamente, existen algunas otras APIs que te permiten diferir la ejecución del código a una tarea posterior. Recomendamos usar postMessage() para tiempos de espera más rápidos.

También puedes dividir el trabajo con requestIdleCallback(), pero programa las tareas con la prioridad más baja y solo durante el tiempo de inactividad del navegador, lo que significa que, si el subproceso principal está especialmente ocupado, es posible que las tareas programadas con requestIdleCallback() nunca se ejecuten.

Use async/await para crear puntos de rendimiento

Para asegurarte de que las tareas importantes para el usuario ocurran antes que las de menor prioridad, realiza las tareas en el subproceso principal mediante la interrupción breve de la lista de tareas en cola para darle al navegador la oportunidad de ejecutar tareas más importantes.

La manera más clara de hacerlo implica un Promise que se resuelve con una llamada a setTimeout():

function yieldToMain () {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

En la función saveSettings(), puedes ceder el paso al subproceso principal después de cada paso si await la función yieldToMain() después de cada llamada a función. Esto divide efectivamente tu tarea larga en varias tareas:

async function saveSettings () {
  // Create an array of functions to run:
  const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
  ]

  // Loop over the tasks:
  while (tasks.length > 0) {
    // Shift the first task off the tasks array:
    const task = tasks.shift();

    // Run the task:
    task();

    // Yield to the main thread:
    await yieldToMain();
  }
}

Punto clave: No es necesario que produzcas el rendimiento después de cada llamada a función. Por ejemplo, si ejecutas dos funciones que generan actualizaciones críticas de la interfaz de usuario, es probable que no quieras obtener entre ellas. Si puedes, primero deja que ese trabajo se ejecute primero y, luego, considera generar entre funciones que realicen tareas en segundo plano o trabajos menos críticos que el usuario no vea.

La misma función saveSettings del generador de perfiles de rendimiento de Chrome, ahora con yield
    La tarea ahora se divide en cinco tareas independientes, una para cada función.
La función saveSettings() ahora ejecuta sus funciones secundarias como tareas independientes.

Una API de programador dedicada

Las APIs mencionadas hasta ahora pueden ayudarte a dividir las tareas, pero tienen una desventaja importante: cuando cedes al subproceso principal aplazando el código para que se ejecute en una tarea posterior, ese código se agrega al final de la lista de tareas en cola.

Si controlas todo el código de tu página, puedes crear tu propio programador para priorizar las tareas. Sin embargo, las secuencias de comandos de terceros no usarán tu programador, por lo que no puedes priorizar el trabajo en ese caso. Solo puedes dividirlo o cederlo a las interacciones del usuario.

Navegadores compatibles

  • 94
  • 94
  • x

Origen

La API de Scheduler ofrece la función postTask(), que permite una programación más detallada de las tareas y puede ayudar al navegador a priorizar el trabajo para que las tareas de baja prioridad cedan al subproceso principal. postTask() usa promesas y acepta una configuración priority.

La API de postTask() tiene tres prioridades disponibles:

  • 'background' para las tareas de menor prioridad.
  • 'user-visible' para tareas de prioridad media Esta es la opción predeterminada si no se establece un priority.
  • 'user-blocking' para tareas esenciales que se deben ejecutar con prioridad alta.

En el siguiente código de ejemplo, se usa la API de postTask() para ejecutar tres tareas con la prioridad más alta posible y las dos restantes con la prioridad más baja posible:

function saveSettings () {
  // Validate the form at high priority
  scheduler.postTask(validateForm, {priority: 'user-blocking'});

  // Show the spinner at high priority:
  scheduler.postTask(showSpinner, {priority: 'user-blocking'});

  // Update the database in the background:
  scheduler.postTask(saveToDatabase, {priority: 'background'});

  // Update the user interface at high priority:
  scheduler.postTask(updateUI, {priority: 'user-blocking'});

  // Send analytics data in the background:
  scheduler.postTask(sendAnalytics, {priority: 'background'});
};

Aquí, la prioridad de las tareas está programada para que las tareas priorizadas por el navegador, como las interacciones del usuario, puedan funcionar.

La función SaveSettings que se muestra en el generador de perfiles de rendimiento de Chrome, pero el uso de postTask. postTask divide cada ejecución de saveSettings y las prioriza para que una interacción del usuario pueda ejecutarse sin que se bloquee.
Cuando se ejecuta saveSettings(), la función programa las llamadas a funciones individuales con postTask(). El trabajo crítico para el usuario se programa con prioridad alta, mientras que el trabajo que el usuario no conoce está programado para ejecutarse en segundo plano. Esto permite que las interacciones del usuario se ejecuten más rápido, ya que el trabajo se divide y se prioriza de forma adecuada.

También puedes crear instancias de diferentes objetos TaskController que compartan prioridades entre las tareas, incluida la capacidad de cambiar las prioridades para diferentes instancias de TaskController según sea necesario.

Rendimiento integrado con continuación mediante la próxima API de scheduler.yield()

Punto clave: Para obtener una explicación más detallada de scheduler.yield(), consulta su prueba de origen (desde que concluyó) así como su explicación.

Una adición propuesta a la API de Scheduler es scheduler.yield(), una API diseñada específicamente para proporcionar datos al subproceso principal del navegador. Su uso es similar a la función yieldToMain() que demostró antes en esta página:

async function saveSettings () {
  // Create an array of functions to run:
  const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
  ]

  // Loop over the tasks:
  while (tasks.length > 0) {
    // Shift the first task off the tasks array:
    const task = tasks.shift();

    // Run the task:
    task();

    // Yield to the main thread with the scheduler
    // API's own yielding mechanism:
    await scheduler.yield();
  }
}

Este código resulta familiar, pero, en lugar de usar yieldToMain(), utiliza await scheduler.yield().

Tres diagramas que muestran tareas sin rendir, con rendición y con rendición y continuación. Sin ceder, hay tareas largas. Con el rendimiento, hay más tareas que son más cortas, pero pueden interrumpirse por otras tareas no relacionadas. Con el rendimiento y la continuación, se conserva el orden de ejecución más corto de las tareas.
Cuando usas scheduler.yield(), la ejecución de la tarea se retoma desde donde se interrumpió, incluso después del punto de productividad.

El beneficio de scheduler.yield() es la continuación, lo que significa que si realizas el trabajo en medio de un conjunto de tareas, las otras tareas programadas continúan en el mismo orden después del punto de productividad. Esto evita que las secuencias de comandos de terceros tomen el control del orden en el que se ejecuta el código.

El uso de scheduler.postTask() con priority: 'user-blocking' también tiene una alta probabilidad de continuidad debido a la prioridad alta de user-blocking, por lo que puedes usarlo como alternativa hasta que scheduler.yield() esté disponible de forma más general.

El uso de setTimeout() (o scheduler.postTask() con priority: 'user-visible' o sin priority explícito) programa la tarea en la parte posterior de la cola, lo que permite que otras tareas pendientes se ejecuten antes de la continuación.

Rendimiento por entrada con isInputPending()

Navegadores compatibles

  • 87
  • 87
  • x
  • x

La API de isInputPending() proporciona una forma de verificar si un usuario intentó interactuar con una página y rendir el trabajo solo si hay una entrada pendiente.

Esto permite que JavaScript continúe si no hay entradas pendientes, en lugar de generar y terminar en la parte posterior de la lista de tareas en cola. Esto puede generar mejoras de rendimiento impresionantes, como se detalla en Intent de envío, para sitios que, de lo contrario, no podrían realizar el rendimiento del subproceso principal.

Sin embargo, desde el lanzamiento de esa API, hemos mejorado nuestra comprensión de la producción, especialmente después de la introducción de INP. Ya no recomendamos usar esta API y, en su lugar, recomendamos generar independientemente de si la entrada está pendiente o no. Este cambio en las recomendaciones se debe a varios motivos:

  • Es posible que la API muestre false de forma incorrecta en algunos casos en los que un usuario interactuó.
  • La entrada no es el único caso en el que deberían generarse las tareas. Las animaciones y otras actualizaciones habituales de la interfaz de usuario pueden ser igualmente importantes para proporcionar una página web responsiva.
  • Desde entonces, se introdujeron APIs de rendimiento más integrales, como scheduler.postTask() y scheduler.yield(), para abordar los problemas de rendimiento.

Conclusión

Administrar las tareas es un desafío, pero hacerlo ayuda a que tu página responda con mayor rapidez a las interacciones del usuario. Hay una variedad de técnicas para administrar y priorizar tareas según el caso de uso. Recuerda que estos son los principales puntos que deberás tener en cuenta cuando administres tareas:

  • Cedemos el tiempo al subproceso principal de las tareas críticas para el usuario.
  • Considera experimentar con scheduler.yield().
  • Prioriza las tareas con postTask().
  • Por último, realiza el menor trabajo posible en tus funciones.

Con una o más de estas herramientas, deberías poder estructurar el trabajo en tu aplicación de modo que priorice las necesidades del usuario y, al mismo tiempo, se garantice que se realice un trabajo menos importante. Esto mejora la experiencia del usuario, ya que es más responsiva y más agradable de usar.

Agradecimientos especiales a Philip Walton por su revisión técnica de este documento.

Miniatura de Unsplash, cortesía de Amirali Mirhashemian.