Speed up your async/await: el «async/await hell» esta haciendo que tu API/App sea lenta.

Async/await llegó para quedarse, es el default en casi todos los nuevos proyectos que se hacen con JavaScript pues ofrece una syntax mas sencilla para aquellos que no se sentían muy cómodos con el estilo tan declarativo de las promesas (en especial aquellos que vienen de otros lenguajes de programación)… pero con todo lo bueno siempre hay algo malo y ese es el async/await hell, qué a diferencia con el callback hell, este nuevo hell nos trae una reducción en la velocidad de nuestro código.

¿Cómo luce el async/await hell?

const job1 = await delay(2500);
const job2 = await delay(500);
const job3 = await delay(650);
const job4 = await delay(job3 * 3);
const job5 = await delay(2000);
const job6 = await delay((job3 -job4 / job5) + job1);

Veamos por un momento el extracto de código arriba, la forma en que se utiliza el await en cada trabajo asíncrono es común de ver en los proyectos donde se utiliza esta syntax. Si asumimos que los posibles errores en un trabajo asíncrono están cubiertos en otro lado, casi siempre se termina viendo cómo se espera a que una promesa termine al mismo tiempo de ser declarada… Un gran error en muchos casos.

En un momento simularemos el tiempo que tarda este código y la solución al mismo, esta simulación se llevará a cabo con dos funciones auxiliares:

const delay = time =>
  new Promise(resolve =>
    setTimeout(resolve.bind(null, time), time)
  );

Una función que nos permite simular un delay (asíncrono) en nuestra app, al mismo tiempo retorna el tiempo que el timeout tardaría.

const differenceInSeconds = (startDate, endDate) =>
  Math.abs((startDate.getTime() - endDate.getTime()) / 1000);

Esta función nos servirá para medir el tiempo consumido entre procesos, así confirmaremos si estamos o no haciendo las cosas mas rápido.

Declara promesas sin esperar por ellas.

La razon por la que tu app es mas lenta cuando sufre del async/await hell es porque esperas siempre a que termine una promesa al momento en que la declaras. Si míranos nuevamente el código al principio, vemos se utiliza el await siempre que se utiliza la función asíncrona delay, esto hace que cada proceso tenga que esperar por el anterior sin importar si este es o no dependiente de este proceso, esto termina haciendo que nuestra app espera paso por paso sin dejar que JavaScript se encargue de iniciar nuestros trabajos asíncronos y terminarlos de manera optima.

Una manera de evitar esto es de la siguiente manera:

const job1 = delay(2500);
const job2 = delay(500);

await job1;
await job2;

De este modo estamos permitiendo que ambas promesas (job1 y job2) se inicien al mismo tiempo y terminen cuando necesiten hacerlo. Luego nos aseguramos de esperar a que ambas ya terminaron antes de continuar.

En caso de que se necesite tener el resultado de ambas promesas tenemos el siguiente método:

const job1 = delay(2500);
const job2 = delay(500);

const job1Result = await job1;
const job2Result = await job2;

O también podemos utilizar la función Promise.all (al fin y al cabo async/await son promesas):

const [job1, job2] = Promise.all([delay(2500), delay(500)]);
// de aquí en adelante, job1 y job2 ya serian el resultado de ambas promesas

async/await hell vs non async/await hell

Primero midamos cuanto tiempo tarda en terminar el proceso en el primer código:

(async () => {
  const now = new Date();

  const job1 = await delay(2500);
  const job2 = await delay(500);
  const job3 = await delay(650);
  const job4 = await delay(job3 * 3);
  const job5 = await delay(2000);
  const job6 = await delay((job3 -job4 / job5) + job1);

  const then = new Date();

  console.log(differenceInSeconds(now, then)) // 10.76 segundos
})();

Utilizando las funciones auxiliares que definimos anteriormente y encerrando el código en una función auto invocada vemos que todo el código tarda un total de 10.76 segundos en promedio. Muchos pensaran «10 segundos no es mucho» y ciertamente no lo es, pero en una API una respuesta que tarda ese tiempo puede hacer que tu sitio web se sienta eterno.

Ahora midamos el tiempo que tardaría utilizando otro método de declaración de promesas:

(async () => {
  const now = new Date();

  const job1 = delay(2500);
  const job2 = delay(500);
  const job3 = delay(650);
  const job4 = job3.then(result => delay(result * 3));
  const job5 = delay(2000);
  const job6 = Promise.all([job3, job4, job5, job1])
    .then(([job3, job4, job5, job1]) => delay((job3 -job4 / job5) + job1));

  await job6;

  const then = new Date();

  console.log(differenceInSeconds(now, then)) // 5.75 segundos
})();

El resultado es un proceso que se termina en casi la mitad de tiempo solo al modificar la manera en como declaramos las mismas cosas que antes. Si por el contrario necesitáramos utilizar todos los valores, podríamos utilizar algo así:

const [
  job1Result,
  job2Result,
  job3Result,
  job4Result,
  job5Result,
  job6Result,
] = await Promise.all([job1, job2, job3, job4, job5, job6]);

Para finalizar…

Async/await es perfecto para Javascript pues hace mas fácil los trabajos asíncronos, pero siempre debemos conocer bien la herramienta que estamos utilizando y si sabemos definir bien nuestras funciones, haremos que nuestros procesos sean lo más rápido posibles.

No Comments Yet