¿Existen problemas de rendimiento con la "espera de devolución"?
Veo que hay una regla eslint no-return-await
para no permitirreturn await
.
En la descripción de la regla, indica un return await
agregado."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?
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+0
en lugar de return x
para 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 …;
} …
await
genera rechazos y, en cualquier caso, espera la resolución de la promesa antes de catch
que finally
se ejecuten los controladores. Una llanura return
lo habría ignorado.
Estoy agregando una respuesta porque un comentario sería demasiado largo. Originalmente tuve una explicación muy larga y detallada sobre cómo async
funciona y await
funciona. 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. await
funciona de manera diferente async
porque ejecuta PromiseResolve (similar Promise.resolve
pero es interno). Básicamente, si ejecuta await
una Promesa (una real, no un polyfill), await
no 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 await
siempre devuelve el resultado de una Promesa, no de una Promesa, y evita ajustar las Promesas cuando sea posible. Eso significa que await
siempre se necesita al menos un tic.
Pero eso es await
y async
en realidad no utiliza este bypass. Ahora async
utiliza 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 .then
no lo es Callable
. Si ninguna de las dos cosas es cierta (estás devolviendo un Promise
), realizará un HostMakeJobCallback
y se sumará a then
la Promesa, lo que básicamente significa que estamos agregando una marca. Aclarado, si devuelve una Promesa en una async
funció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().then
fallarí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.
async
Le 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 then
se complete (otro +1). Por eso son 3 ticks.
await
convertirá a Promise
en a, Non-Promise
lo cual es perfecto para async
. Si llama a await a Promise
, lo ejecutará sin realizar ningún tick adicional (+1). Pero await
convertirá a Non-Promise
en a Promise
y luego lo ejecutará. Eso significa que await
siempre 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 async
función siempre incluya al menos un archivo await
. Si no es así, simplemente hazlo sincrónico. Siempre puedes llamar await
a 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);