JavaScript: Wie legt man eine Pause ein (sleep)?

JavaScript hat kein blockierendes sleep, und das mit Absicht: Ein Thread führt deinen Code aus, die Timer und die Klicks — ein Thread, der schläft, hält also alle drei an.

Das, was man nimmt

const sleep = d => new Promise(r => setTimeout(r, d));
await sleep(300);
await sleep(300) took 301 ms

setTimeout in ein Promise gepackt und darauf gewartet. Drei Tokens, keine Abhängigkeit.

Was es tatsächlich anhält

after starting slow(), we are already here at 0 ms
slow done at 200 ms

Nur die async-Funktion, in der es steht. await gibt an die Event-Loop ab, alles andere läuft also weiter — Timer feuern, die Seite bleibt bedienbar, andere abgewartete Arbeit kommt voran. Das ist der Unterschied zu einem sleep() in einer Sprache mit Threads, und es ist ein Feature.

Ohne await hält setTimeout gar nichts an:

the line after setTimeout runs at 0 ms
timer fired at 100 ms

Was ein Busy-Wait kostet

function busySleep(d) { const end = Date.now() + d; while (Date.now() < end) {} }
busySleep(300) took 300 ms
did the 50 ms timer fire during it? false
after yielding, has it fired now? true

Die Schleife blockiert tatsächlich, deshalb greift man danach. Der Preis steht in der zweiten Zeile: Ein Timer, der nach 50 ms fällig war, lief erst nach der Schleife, 250 ms zu spät. Auch sonst konnte nichts laufen — im Browser heißt das eingefrorene Seite, kein Scrollen, keine Klicks, und nach ein paar Sekunden ein “Seite reagiert nicht”-Dialog. Nebenbei brennt es die ganze Zeit einen Kern.

Die Verzögerung ist ein Minimum

sleep(0) actually took 1 ms
sleep(5) actually took 5 ms
10 chained setTimeout(...,0) took 12 ms, not 0

setTimeout plant ein, es verspricht nichts. Der Callback läuft nicht früher als nach der Verzögerung, und erst wenn die aktuelle Arbeit fertig ist. Eine Verzögerung von 0 wird in Node auf 1 ms angehoben; Browser heben zusätzlich auf 4 ms an, sobald Timer mehr als fünf Ebenen tief verschachtelt sind.

Nacheinander oder parallel ist deine Entscheidung

for..of with await : 301 ms (sequential)
map + Promise.all  : 100 ms (parallel)
forEach with async : 1 ms <- did not wait at all

Dreimal 100 ms warten, drei Ergebnisse. for..of mit await wartet jedes Mal. Promise.all startet alle drei und wartet einmal.

Die dritte Zeile ist der Fehler, den man kennen sollte. forEach ruft deinen async-Callback auf, bekommt ein Promise zurück und wirft es weg — es weiß nichts damit anzufangen. Die Schleife ist also sofort fertig, und der Code danach läuft, während die Arbeit noch offen ist. Das sieht exakt aus wie die funktionierende Variante.

Die, die man vielleicht nicht kennt

timers/promises setTimeout(200) took 200 ms

Node bringt einen Promise-basierten Timer in node:timers/promises mit, auf dem Server brauchst du den Wrapper also nicht zu schreiben.

Atomics.wait(..., 200) blocked for 200 ms

Atomics.wait blockiert den Thread wirklich, und anders als die Schleife verbraucht es dabei keine CPU. Im Haupt-Thread eines Browsers ist es nicht erlaubt — was die richtige Einschränkung ist und dir sagt, wo der verbleibende sinnvolle Einsatzort liegt: in einem Worker.

Hinweis zu Netcup (Werbung)

Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).