¿Existen problemas de rendimiento con la "espera de devolución"?

Resuelto sfletche asked hace 7 años • 2 respuestas

Veo que hay una regla eslint no-return-awaitpara no permitirreturn await .

En la descripción de la regla, indica un return awaitagregado."extra time before the overarching Promise resolves or rejects" .

Sin embargo, cuando miro los documentos de funciones de MDNasync , el "Ejemplo simple" muestra un ejemplo que contienereturn await sin ninguna descripción de por qué esto podría ser un problema de rendimiento.

Esreturn await un problema de rendimiento real como sugieren los documentos de Eslint?

Y si es así, ¿cómo?

sfletche avatar Apr 12 '17 00:04 sfletche
Aceptado

No, no hay ningún problema de rendimiento . Es sólo una operación extra innecesaria. Puede que tarde un poco más en ejecutarse, pero apenas debería notarse. Es similar a return x+0en lugar de return xpara un número entero x. O mejor dicho, exactamente equivalente a lo inútil.then(x => x) .

No hace ningún daño real, pero lo consideraría un mal estilo y una señal de que el autor no comprende completamente las promesas y async/o await.

Sin embargo, hay un caso en el que marca una diferencia importante:

try {
    …
    return await …;
} …

awaitgenera rechazos y, en cualquier caso, espera la resolución de la promesa antes de catchque finallyse ejecuten los controladores. Una llanura returnlo habría ignorado.

Bergi avatar May 15 '2017 16:05 Bergi

Estoy agregando una respuesta porque un comentario sería demasiado largo. Originalmente tuve una explicación muy larga y detallada sobre cómo asyncfunciona y awaitfunciona. Pero es tan complicado que los datos reales pueden ser más fáciles de entender. Así que aquí está la explicación simplificada. Nota: Esto se ejecuta en Chrome v97, FireFox v95 y Node v16 con los mismos resultados.

La respuesta a qué es más rápido: depende de lo que devuelvas y de cómo lo llames. awaitfunciona de manera diferente asyncporque ejecuta PromiseResolve (similar Promise.resolvepero es interno). Básicamente, si ejecuta awaituna Promesa (una real, no un polyfill), awaitno intente ajustarla. Ejecuta la promesa tal cual. Eso se salta un tic. Este es un cambio "más nuevo" de 2018. En resumen, la evaluación awaitsiempre devuelve el resultado de una Promesa, no de una Promesa, y evita ajustar las Promesas cuando sea posible. Eso significa que awaitsiempre se necesita al menos un tic.

Pero eso es awaity asyncen realidad no utiliza este bypass. Ahora asyncutiliza el viejo PromiseCapability Record . Nos importa cómo esto resuelve las promesas. Los puntos clave son que comenzará a cumplirse instantáneamente si la resolución es "no Object" o si .thenno lo es Callable. Si ninguna de las dos cosas es cierta (estás devolviendo un Promise), realizará un HostMakeJobCallbacky se sumará a thenla Promesa, lo que básicamente significa que estamos agregando una marca. Aclarado, si devuelve una Promesa en una asyncfunción, agregará un tick adicional, pero no si devuelve una No Promesa.

Entonces, con todo ese prefacio (y esta es la versión simplificada), aquí está el cuadro de cuántos ticks faltan para que await foo()se devuelva su llamada:

Sin promesa Promesa
() => resultado 1 1
asíncrono () => resultado 1 3
asíncrono () => esperar resultado 2 2

Esto se prueba con await foo(). También puedes probar con foo().then(...), pero las marcas son las mismas. (Si no usa un await, entonces la función de sincronización sería 0. Aunque foo().thenfallaría, por lo que necesitamos algo real para probar). Eso significa que nuestro piso es 1.

Si entendiste mis explicaciones anteriores (con suerte), esto tendrá sentido. La función de sincronización tiene sentido porque en ningún momento de la función solicitamos una ejecución en pausa: await foo()tomará 1 tic.

asyncLe gustan las no promesas y las espera. Regresará inmediatamente si encuentra uno. Pero si encuentra una Promesa, se unirá a la de esa Promesa then. Eso significa que ejecutará la Promesa (+1) y luego esperará a que thense complete (otro +1). Por eso son 3 ticks.

awaitconvertirá a Promiseen a, Non-Promiselo cual es perfecto para async. Si llama a await a Promise, lo ejecutará sin realizar ningún tick adicional (+1). Pero awaitconvertirá a Non-Promiseen a Promisey luego lo ejecutará. Eso significa que awaitsiempre hay un tic, independientemente de cómo lo llames.

Entonces, en conclusión, si desea la ejecución más rápida, debe asegurarse de que su asyncfunción siempre incluya al menos un archivo await. Si no es así, simplemente hazlo sincrónico. Siempre puedes llamar awaita cualquier función sincrónica. Ahora, si realmente desea modificar el rendimiento y va a utilizar async, debe asegurarse de devolver siempre un Non-Promise, no un Promise. Si está devolviendo un Promise, conviértalo primero con await. Dicho esto, puedes mezclar y combinar así:


async function getData(id) {
  const cache = myCacheMap.get(id);
  if (cache) return cache; // NonPromise returns immediately (1 tick)
  
  // return fetch(id); // Bad: Promise returned in async (3 ticks)
  return await fetch(id); // Good: Promise to NonPromise via await (2 ticks)
}

Con eso en mente, tengo un montón de código para reescribir :)


Referencias:

https://v8.dev/blog/fast-async

https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-async-functions-operaciones-abstractas-async-function-start


Prueba:

async function test(name, fn) {
  let tick = 0;
  const tock = () => tick++;
  Promise.resolve().then(tock).then(tock).then(tock);

  const p = await fn();
  console.assert(p === 42);
  console.log(name, tick);
}

await Promise.all([
  test('nonpromise-sync', () => 42),
  test('nonpromise-async', async () => 42),
  test('nonpromise-async-await', async () => await 42),
  test('promise-sync', () => Promise.resolve(42)),
  test('promise-async', async () => Promise.resolve(42)),
  test('promise-async-await', async () => await Promise.resolve(42)),
]);

setTimeout(() => {}, 100);
ShortFuse avatar Feb 03 '2022 22:02 ShortFuse