Schnellere asynchrone Funktionen und Versprechen
Asynchrone Verarbeitung in JavaScript hatte traditionell den Ruf, nicht besonders schnell zu sein. Noch schlimmer war es, Live-JavaScript-Anwendungen – insbesondere Node.js-Server – zu debuggen, besonders wenn es um asynchrone Programmierung geht. Glücklicherweise ändern sich die Zeiten. Dieser Artikel untersucht, wie wir asynchrone Funktionen und Versprechen in V8 (und teilweise auch in anderen JavaScript-Engines) optimiert haben und beschreibt, wie wir die Debugging-Erfahrung für asynchronen Code verbessert haben.
Hinweis: Wenn Sie eine Präsentation lieber ansehen als Artikel zu lesen, genießen Sie das folgende Video! Wenn nicht, überspringen Sie das Video und lesen Sie weiter.
Ein neuer Ansatz zur asynchronen Programmierung
Von Callbacks zu Versprechen zu asynchronen Funktionen
Bevor Versprechen Teil der JavaScript-Sprache waren, wurden Callback-basierte APIs häufig für asynchronen Code verwendet, insbesondere in Node.js. Hier ein Beispiel:
function handler(done) {
validateParams((error) => {
if (error) return done(error);
dbQuery((error, dbResults) => {
if (error) return done(error);
serviceCall(dbResults, (error, serviceResults) => {
console.log(result);
done(error, serviceResults);
});
});
});
}
Das spezifische Muster der Verwendung von tief verschachtelten Callbacks auf diese Weise wird häufig als „Callback-Hölle“ bezeichnet, weil es den Code weniger lesbar und schwer wartbar macht.
Glücklicherweise kann derselbe Code, jetzt da Versprechen Teil der JavaScript-Sprache sind, auf elegantere und wartbarere Weise geschrieben werden:
function handler() {
return validateParams()
.then(dbQuery)
.then(serviceCall)
.then(result => {
console.log(result);
return result;
});
}
Noch vor kurzem hat JavaScript Unterstützung für asynchrone Funktionen erhalten. Der obige asynchrone Code kann jetzt auf eine Art geschrieben werden, die sehr ähnlich wie synchroner Code aussieht:
async function handler() {
await validateParams();
const dbResults = await dbQuery();
const results = await serviceCall(dbResults);
console.log(results);
return results;
}
Mit asynchronen Funktionen wird der Code kürzer und der Kontroll- und Datenfluss ist viel leichter zu verfolgen, obwohl die Ausführung immer noch asynchron ist. (Beachten Sie, dass die JavaScript-Ausführung weiterhin in einem einzelnen Thread erfolgt, was bedeutet, dass asynchrone Funktionen selbst keine physischen Threads erstellen.)
Von Ereignis-Listener-Callbacks zu asynchroner Iteration
Ein weiteres asynchrones Paradigma, das besonders häufig in Node.js vorkommt, ist das der ReadableStreams
. Hier ein Beispiel:
const http = require('http');
http.createServer((req, res) => {
let body = '';
req.setEncoding('utf8');
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', () => {
res.write(body);
res.end();
});
}).listen(1337);
Dieser Code kann etwas schwer zu folgen sein: Die eingehenden Daten werden in Chunks verarbeitet, die nur innerhalb von Callbacks zugänglich sind, und das Ende des Streams wird ebenfalls innerhalb eines Callbacks signalisiert. Es ist leicht, hier Fehler einzuführen, wenn man nicht erkennt, dass die Funktion sofort beendet wird und die eigentliche Verarbeitung in den Callbacks stattfinden muss.
Glücklicherweise kann ein cooles neues ES2018-Feature namens asynchrone Iteration diesen Code vereinfachen:
const http = require('http');
http.createServer(async (req, res) => {
try {
let body = '';
req.setEncoding('utf8');
for await (const chunk of req) {
body += chunk;
}
res.write(body);
res.end();
} catch {
res.statusCode = 500;
res.end();
}
}).listen(1337);
Anstatt die Logik, die sich mit der eigentlichen Anfrageverarbeitung befasst, in zwei verschiedene Callbacks – das 'data'
und das 'end'
Callback – zu setzen, können wir jetzt alles in eine einzelne asynchrone Funktion setzen und die neue for await…of
Schleife verwenden, um über die Chunks asynchron zu iterieren. Wir haben auch einen try-catch
Block hinzugefügt, um das Problem mit unhandledRejection
zu vermeiden1.
Sie können diese neuen Funktionen bereits heute in der Produktion verwenden! Async-Funktionen werden vollständig unterstützt ab Node.js 8 (V8 v6.2 / Chrome 62), und async-Iteratoren und Generatoren werden vollständig unterstützt ab Node.js 10 (V8 v6.8 / Chrome 68)!
Verbesserungen der asynchronen Leistung
Wir konnten die Leistung von asynchronem Code zwischen V8 v5.5 (Chrome 55 & Node.js 7) und V8 v6.8 (Chrome 68 & Node.js 10) erheblich verbessern. Wir haben ein Leistungsniveau erreicht, bei dem Entwickler diese neuen Programmierparadigmen sicher verwenden können, ohne sich um die Geschwindigkeit sorgen zu müssen.
Das obige Diagramm zeigt den Doxbee-Benchmark, der die Leistung von promise-lastigem Code misst. Beachten Sie, dass die Diagramme die Ausführungszeit visualisieren, was bedeutet, dass niedriger besser ist.
Die Ergebnisse des Parallel-Benchmarks, der speziell die Leistung von Promise.all()
testet, sind sogar noch spannender:
Wir konnten die Leistung von Promise.all
um den Faktor 8× verbessern.
Die obigen Benchmarks sind jedoch synthetische Mikro-Benchmarks. Das V8-Team interessiert sich stärker dafür, wie sich unsere Optimierungen auf die Real-World-Leistung von echtem Benutzer-Code auswirken.
Das obige Diagramm zeigt die Leistung einiger populärer HTTP-Middleware-Frameworks, die intensiv Promises und async
-Funktionen verwenden. Beachten Sie, dass dieses Diagramm die Anzahl der Anfragen/Sekunde zeigt, sodass im Gegensatz zu den vorherigen Diagrammen höher besser ist. Die Leistung dieser Frameworks hat sich zwischen Node.js 7 (V8 v5.5) und Node.js 10 (V8 v6.8) erheblich verbessert.
Diese Leistungsverbesserungen sind das Ergebnis von drei wichtigen Errungenschaften:
- TurboFan, der neue optimierende Compiler 🎉
- Orinoco, der neue Garbage Collector 🚛
- ein Node.js 8-Fehler, der dazu führte, dass
await
Mikroticks übersprang 🐛
Als wir TurboFan eingeführt haben in Node.js 8, führte dies zu einem erheblichen Leistungsanstieg auf ganzer Linie.
Wir haben auch an einem neuen Garbage Collector namens Orinoco gearbeitet, der die Arbeit der Garbage Collection vom Haupt-Thread verlagert und somit die Anfragenbearbeitung erheblich verbessert.
Und last but not least gab es einen praktischen Fehler in Node.js 8, der dazu führte, dass await
in einigen Fällen Mikroticks übersprang, was zu einer besseren Leistung führte. Der Fehler begann als unbeabsichtigter Spezifikationsverstoß, gab uns jedoch später die Idee für eine Optimierung. Beginnen wir mit der Erklärung des fehlerhaften Verhaltens:
Hinweis: Das folgende Verhalten war zum Zeitpunkt des Schreibens gemäß der JavaScript-Spezifikation korrekt. Seitdem wurde unser Spezifikationsvorschlag akzeptiert, und das folgende "fehlerhafte" Verhalten ist nun korrekt.
const p = Promise.resolve();
(async () => {
await p; console.log('after:await');
})();
p.then(() => console.log('tick:a'))
.then(() => console.log('tick:b'));
Das obige Programm erstellt ein erfülltes Promise p
und await
dessen Ergebnis, hängt aber auch zwei Handler daran an. In welcher Reihenfolge würden Sie erwarten, dass die console.log
-Aufrufe ausgeführt werden?
Da p
erfüllt ist, könnten Sie erwarten, dass zuerst 'after:await'
und dann die 'tick'
s ausgegeben werden. Tatsächlich ist das das Verhalten, das Sie in Node.js 8 erhalten würden:
Obwohl dieses Verhalten intuitiv erscheint, ist es gemäß der Spezifikation nicht korrekt. Node.js 10 implementiert das korrekte Verhalten, bei dem zuerst die angehängten Handler ausgeführt werden und erst danach mit der asynchronen Funktion fortgefahren wird.
Dieses „korrekte Verhalten“ ist möglicherweise nicht sofort offensichtlich und war tatsächlich überraschend für JavaScript-Entwickler, es verdient daher eine Erklärung. Bevor wir in die magische Welt der Promises und Async-Funktionen eintauchen, beginnen wir mit einigen Grundlagen.
Aufgaben vs. Mikrotasks
Auf hoher Ebene gibt es Aufgaben und Mikrotasks in JavaScript. Aufgaben bearbeiten Ereignisse wie I/O und Timer und werden nacheinander ausgeführt. Mikrotasks implementieren verzögerte Ausführung für async
/await
und Promises und werden am Ende jeder Aufgabe ausgeführt. Die Mikrotask-Warteschlange wird immer geleert, bevor die Ausführung an die Ereignisschleife zurückkehrt.
Weitere Details finden Sie in Jake Archibalds Erklärung zu Tasks, Microtasks, Queues und Schedules im Browser. Das Aufgabenmodell in Node.js ist sehr ähnlich.
Async-Funktionen
Laut MDN ist eine Async-Funktion eine Funktion, die asynchron arbeitet und ein implizites Versprechen verwendet, um ihr Ergebnis zurückzugeben. Async-Funktionen sollen asynchronen Code wie synchronen Code aussehen lassen und dabei einen Teil der Komplexität der asynchronen Verarbeitung vor dem Entwickler verbergen.
Die einfachste mögliche Async-Funktion sieht so aus:
async function computeAnswer() {
return 42;
}
Beim Aufruf gibt diese ein Versprechen zurück, und Sie können dessen Wert wie bei jedem anderen Versprechen abrufen.
const p = computeAnswer();
// → Versprechen
p.then(console.log);
// druckt 42 im nächsten Durchgang
Sie gelangen erst beim nächsten Laufen von Microtasks an den Wert dieses Versprechens p
. Mit anderen Worten: Das obige Programm ist semantisch äquivalent zu der Verwendung von Promise.resolve
mit dem Wert:
function computeAnswer() {
return Promise.resolve(42);
}
Die wahre Stärke von Async-Funktionen kommt durch await
-Ausdrücke, die die Ausführung der Funktion so lange pausieren, bis ein Versprechen aufgelöst ist, und danach fortgesetzt wird. Der Wert von await
ist der des erfüllten Versprechens. Hier ein Beispiel, das zeigt, was das bedeutet:
async function fetchStatus(url) {
const response = await fetch(url);
return response.status;
}
Die Ausführung von fetchStatus
wird an der await
-Stelle ausgesetzt und später fortgesetzt, wenn das fetch
-Versprechen erfüllt wird. Dies ist mehr oder weniger gleichbedeutend mit dem Verketten eines Handlers an das von fetch
zurückgegebene Versprechen.
function fetchStatus(url) {
return fetch(url).then(response => response.status);
}
Dieser Handler enthält den Code, der dem await
in der Async-Funktion folgt.
Normalerweise würden Sie ein Promise
an await
übergeben, aber Sie können tatsächlich auf jeden beliebigen JavaScript-Wert warten. Wenn der Wert des Ausdrucks nach dem await
kein Versprechen ist, wird er in ein Versprechen umgewandelt. Das bedeutet, Sie können await 42
machen, wenn Sie das möchten:
async function foo() {
const v = await 42;
return v;
}
const p = foo();
// → Versprechen
p.then(console.log);
// druckt letztendlich `42`
Interessanterweise funktioniert await
mit jedem „thenable“, also jedem Objekt mit einer then
-Methode, auch wenn es kein echtes Versprechen ist. So können Sie lustige Dinge wie einen asynchronen Sleep implementieren, der die tatsächlich verbrachte Schlafzeit misst:
class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
then(resolve, reject) {
const startTime = Date.now();
setTimeout(() => resolve(Date.now() - startTime),
this.timeout);
}
}
(async () => {
const actualTime = await new Sleep(1000);
console.log(actualTime);
})();
Sehen wir uns an, was V8 für await
hinter den Kulissen tut, entsprechend der Spezifikation. Hier ist eine einfache Async-Funktion foo
:
async function foo(v) {
const w = await v;
return w;
}
Beim Aufruf wird der Parameter v
in ein Versprechen eingepackt und die Ausführung der Async-Funktion ausgesetzt, bis dieses Versprechen aufgelöst ist. Sobald das passiert, wird die Ausführung der Funktion wieder aufgenommen und w
erhält den Wert des erfüllten Versprechens. Dieser Wert wird dann von der Async-Funktion zurückgegeben.
await
hinter den Kulissen
V8 markiert diese Funktion zunächst als resumable, was bedeutet, dass die Ausführung unterbrochen und später (an await
-Punkten) fortgesetzt werden kann. Anschließend erstellt es das sogenannte implicit_promise
, das das Versprechen ist, das bei Aufruf der Async-Funktion zurückgegeben wird und schließlich den von der Async-Funktion erzeugten Wert erhält.
Dann kommt der interessante Teil: das eigentliche await
. Zuerst wird der Wert, der an await
übergeben wurde, in ein Versprechen eingepackt. Anschließend werden Handler an dieses eingepackte Versprechen angehängt, um die Funktion fortzusetzen, sobald das Versprechen erfüllt ist, und die Ausführung der Async-Funktion wird ausgesetzt und das implicit_promise
wird dem Aufrufer zurückgegeben. Sobald das promise
erfüllt ist, wird die Ausführung der Async-Funktion mit dem Wert w
aus dem promise
fortgesetzt und das implicit_promise
mit w
aufgelöst.
Zusammenfassend bestehen die ersten Schritte für await v
aus:
- Verpacken von
v
- dem Wert, der anawait
übergeben wird - in ein Versprechen. - Anhängen von Handlern, um die Async-Funktion später fortzusetzen.
- Aussetzen der Async-Funktion und Zurückgeben des
implicit_promise
an den Aufrufer.
Gehen wir die einzelnen Vorgänge Schritt für Schritt durch. Nehmen wir an, dass das, worauf gewartet wird, bereits ein Versprechen ist, das mit dem Wert 42
erfüllt wurde. Dann erstellt die Engine ein neues promise
und löst es mit dem auf, worauf gewartet wird. Dies macht eine von der Spezifikation als PromiseResolveThenableJob
bezeichnete verzögerte Verkettung dieser Versprechen im nächsten Durchgang.
Dann erzeugt die Engine ein weiteres sogenanntes wegwerf
-Versprechen. Es wird wegwerf genannt, weil nichts daran angehängt wird — es ist vollständig intern für die Engine. Dieses wegwerf
-Versprechen wird dann an das promise
gekoppelt, mit den entsprechenden Handlern, um die asynchrone Funktion fortzusetzen. Diese performPromiseThen
-Operation ist im Wesentlichen das, was Promise.prototype.then()
hinter den Kulissen tut. Schließlich wird die Ausführung der asynchronen Funktion angehalten, und die Kontrolle kehrt zum Aufrufer zurück.
Die Ausführung wird beim Aufrufer fortgesetzt, und schließlich wird der Callstack leer. Dann beginnt die JavaScript-Engine mit der Ausführung der Mikrotasks: Sie führt den zuvor geplanten PromiseResolveThenableJob
aus, der einen neuen PromiseReactionJob
plant, um das promise
an den Wert weiterzugeben, der an await
übergeben wurde. Danach kehrt die Engine zur Verarbeitung der Mikrotask-Warteschlange zurück, da diese geleert werden muss, bevor die Haupt-Event-Schleife fortgesetzt wird.
Als nächstes folgt der PromiseReactionJob
, der das promise
mit dem Wert aus dem Promise erfüllt, den wir await
en — in diesem Fall 42
— und die Reaktion auf das wegwerf
-Versprechen plant. Die Engine kehrt dann erneut zur Mikrotask-Schleife zurück, die eine letzte zu verarbeitende Mikrotask enthält.
Nun propagiert dieser zweite PromiseReactionJob
die Auflösung an das wegwerf
-Versprechen und setzt die angehaltene Ausführung der asynchronen Funktion fort, wobei der Wert 42
vom await
zurückgegeben wird.
Zusammenfassend: Für jedes await
muss die Engine zwei zusätzliche Versprechen erstellen (selbst wenn die rechte Seite bereits ein Versprechen ist), und es werden mindestens drei Ticks in der Mikrotask-Warteschlange benötigt. Wer hätte gedacht, dass ein einzelner await
-Ausdruck so viel Overhead verursacht?!
Schauen wir uns an, woher dieser Overhead kommt. Die erste Zeile ist dafür verantwortlich, das Wrapper-Versprechen zu erstellen. Die zweite Zeile löst dieses Wrapper-Versprechen sofort mit dem await
-Wert v
. Diese beiden Zeilen sind verantwortlich für ein zusätzliches Versprechen und zwei der drei Mikroticks. Das ist ziemlich teuer, wenn v
bereits ein Versprechen ist (was der Normalfall ist, da Anwendungen normalerweise auf Versprechen await
en). Im unwahrscheinlichen Fall, dass ein Entwickler z. B. 42
await
et, muss die Engine es dennoch in ein Versprechen einpacken.
Es stellt sich heraus, dass in der Spezifikation bereits eine promiseResolve
-Operation vorhanden ist, die nur dann eine Verpackung vornimmt, wenn dies erforderlich ist:
Diese Operation gibt Versprechen unverändert zurück und verpackt andere Werte nur nach Bedarf. Auf diese Weise sparen Sie eines der zusätzlichen Versprechen sowie zwei Ticks in der Mikrotask-Warteschlange, falls der an await
übergebene Wert bereits ein Versprechen ist. Dieses neue Verhalten ist bereits in V8 v7.2 standardmäßig aktiviert. Für V8 v7.1 kann das neue Verhalten mit dem Flag --harmony-await-optimization
aktiviert werden. Wir haben diese Änderung auch für die ECMAScript-Spezifikation vorgeschlagen.
So funktioniert das neue und verbesserte await
im Hintergrund Schritt für Schritt:
Angenommen, wir await
en erneut ein Versprechen, das mit 42
erfüllt wurde. Dank der Magie von promiseResolve
verweist das promise
jetzt einfach auf dasselbe Versprechen v
, sodass in diesem Schritt nichts zu tun ist. Danach fährt die Engine genau wie zuvor fort, erstellt das wegwerf
-Versprechen, plant einen PromiseReactionJob
, um die asynchrone Funktion im nächsten Tick der Mikrotask-Warteschlange fortzusetzen, setzt die Ausführung der Funktion aus und kehrt zum Aufrufer zurück.
Dann, wenn schließlich alle JavaScript-Ausführungen abgeschlossen sind, beginnt die Engine mit der Ausführung der Mikrotasks, indem der PromiseReactionJob
ausgeführt wird. Dieser Job propagiert die Auflösung von promise
zu wegwerf
und setzt die Ausführung der asynchronen Funktion fort, wobei 42
vom await
geholt wird.
Diese Optimierung vermeidet die Notwendigkeit, ein Wrapper-Versprechen zu erstellen, wenn der an await
übergebene Wert bereits ein Versprechen ist, und in diesem Fall reduzieren wir uns von drei Mikroticks auf nur einen Mikrotick. Dieses Verhalten ist ähnlich dem, was Node.js 8 tut, mit dem Unterschied, dass es jetzt kein Bug mehr ist — es ist jetzt eine Optimierung, die standardisiert wird!
Es fühlt sich immer noch falsch an, dass die Engine dieses wegwerf
-Versprechen erstellen muss, obwohl es vollständig intern für die Engine ist. Es stellte sich heraus, dass das wegwerf
-Versprechen nur dort war, um die API-Beschränkungen der internen performPromiseThen
-Operation in der Spezifikation zu erfüllen.
Dies wurde kürzlich in einer redaktionellen Änderung der ECMAScript-Spezifikation behandelt. Engines müssen das throwaway
-Promise für await
nicht mehr erstellen — meistens2.
Ein Vergleich von await
in Node.js 10 mit dem optimierten await
, das wahrscheinlich in Node.js 12 enthalten sein wird, zeigt die Auswirkungen dieser Änderung auf die Leistung:
async
/await
übertrifft nun handgeschriebenen Promise-Code. Das Hauptanliegen hier ist, dass wir den Overhead von asynchronen Funktionen erheblich reduziert haben — nicht nur in V8, sondern auch in allen JavaScript-Engines, indem wir die Spezifikation angepasst haben.
Update: Ab V8 v7.2 und Chrome 72 ist --harmony-await-optimization
standardmäßig aktiviert. Der Patch zur ECMAScript-Spezifikation wurde integriert.
Verbesserte Entwicklererfahrung
Neben der Leistung interessieren sich JavaScript-Entwickler auch dafür, Probleme diagnostizieren und beheben zu können, was nicht immer einfach ist, wenn man mit asynchronem Code arbeitet. Chrome DevTools unterstützt asynchrone Stack-Traces, also Stack-Traces, die nicht nur den aktuellen synchronen Teil des Stacks umfassen, sondern auch den asynchronen Teil:
Das ist eine äußerst nützliche Funktion während der lokalen Entwicklung. Diese Methode hilft Ihnen jedoch nicht wirklich, wenn die Anwendung bereitgestellt ist. Während des Debuggens nach einem Absturz sehen Sie in Ihren Protokolldateien nur die Ausgabe von Error#stack
, die Ihnen nichts über die asynchronen Teile mitteilt.
Wir haben kürzlich an kostenlosen asynchronen Stack-Traces gearbeitet, die die Eigenschaft Error#stack
mit Aufrufen asynchroner Funktionen anreichern. „Kostenlos“ klingt spannend, nicht wahr? Wie kann es kostenlos sein, wenn die Chrome-DevTools-Funktion mit erheblichem Overhead verbunden ist? Betrachten Sie dieses Beispiel, in dem foo
bar
asynchron aufruft und bar
nach dem await
eines Promises eine Ausnahme auslöst:
async function foo() {
await bar();
return 42;
}
async function bar() {
await Promise.resolve();
throw new Error('BEEP BEEP');
}
foo().catch(error => console.log(error.stack));
Die Ausführung dieses Codes in Node.js 8 oder Node.js 10 führt zu folgendem Output:
$ node index.js
Error: BEEP BEEP
at bar (index.js:8:9)
at process._tickCallback (internal/process/next_tick.js:68:7)
at Function.Module.runMain (internal/modules/cjs/loader.js:745:11)
at startup (internal/bootstrap/node.js:266:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:595:3)
Beachten Sie, dass obwohl der Aufruf von foo()
den Fehler verursacht, foo
überhaupt nicht Teil des Stack-Traces ist. Dies macht es JavaScript-Entwicklern schwer, eine Fehleranalyse nach einem Absturz durchzuführen, unabhängig davon, ob Ihr Code in einer Webanwendung oder in einem Cloud-Container eingesetzt wird.
Das Interessante hier ist, dass die Engine weiß, wo sie fortfahren muss, wenn bar
fertig ist: direkt nach dem await
in der Funktion foo
. Zufällig ist das auch der Punkt, an dem die Funktion foo
pausiert wurde. Die Engine kann diese Informationen nutzen, um Teile des asynchronen Stack-Traces, nämlich die await
-Stellen, zu rekonstruieren. Mit dieser Änderung wird der Output wie folgt:
$ node --async-stack-traces index.js
Error: BEEP BEEP
at bar (index.js:8:9)
at process._tickCallback (internal/process/next_tick.js:68:7)
at Function.Module.runMain (internal/modules/cjs/loader.js:745:11)
at startup (internal/bootstrap/node.js:266:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:595:3)
at async foo (index.js:2:3)
Im Stack-Trace kommt die oberste Funktion zuerst, gefolgt vom Rest des synchronen Stack-Traces und schließlich vom asynchronen Aufruf von bar
in der Funktion foo
. Diese Änderung wird in V8 hinter dem neuen --async-stack-traces
-Flag implementiert. Update: Ab V8 v7.3 ist --async-stack-traces
standardmäßig aktiviert.
Wenn Sie dies jedoch mit dem asynchronen Stacktrace in den Chrome DevTools oben vergleichen, werden Sie feststellen, dass die tatsächliche Aufrufstelle von foo
im asynchronen Teil des Stacktraces fehlt. Wie bereits erwähnt, nutzt dieser Ansatz die Tatsache, dass bei await
die Fortsetzungs- und Unterbrechungsorte identisch sind – bei regulären Promise#then()
- oder Promise#catch()
-Aufrufen ist dies jedoch nicht der Fall. Weitere Informationen finden Sie in der Erklärung von Mathias Bynens darüber, warum await
Promise#then()
überlegen ist.
Fazit
Wir haben asynchrone Funktionen dank zwei wesentlicher Optimierungen schneller gemacht:
- die Entfernung von zwei zusätzlichen Microticks, und
- die Entfernung des
throwaway
-Promises.
Darüber hinaus haben wir die Entwicklererfahrung mittels kostenloser asynchroner Stacktraces verbessert, die mit await
in asynchronen Funktionen und Promise.all()
funktionieren.
Außerdem haben wir einige nützliche Leistungstipps für JavaScript-Entwickler:
- bevorzugen Sie
async
-Funktionen undawait
gegenüber handgeschriebenem Promise-Code, und - verwenden Sie die native Promise-Implementierung der JavaScript-Engine, um von den Optimierungen zu profitieren, d.h. das Vermeiden von zwei Microticks für
await
.
Footnotes
-
Danke an Matteo Collina dafür, dass er uns auf dieses Problem hingewiesen hat. ↩
-
V8 muss das
throwaway
-Promise noch erstellen, wennasync_hooks
in Node.js verwendet werden, da diebefore
- undafter
-Hooks im Kontext desthrowaway
-Promises ausgeführt werden. ↩