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.
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.
Para evitarlo, divide cada tarea larga en tareas más pequeñas que tarden menos en ejecutarse. Esto se llama dividir las tareas largas.
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.
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();
}
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.
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.
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 unpriority
.'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.
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()
.
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()
yscheduler.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.