Pętla zdarzeń w JavaScript — jak naprawdę działa asynchroniczność
Zrozum Event Loop bez skrótów myślowych. Call Stack, taski, microtaski, await, timery i renderowanie wyjaśnione tak, żeby dało się to później obronić na rozmowie i w praktyce.
"Wyjaśnij proszę Event Loop" to jedno z tych pytań, które bardzo często wracają na rozmowach technicznych dotyczących JavaScript. I bardzo często odpowiedź kończy się na skrócie: "JavaScript jest jednowątkowy, więc ma kolejkę...".
Ale co to naprawdę znaczy? Dlaczego setTimeout(fn, 0) nie wykonuje się natychmiast? Dlaczego Promise rozwiązuje się przed setTimeout? Czym są microtasks?
W tym artykule wyjaśnię Event Loop od podstaw, z wizualizacjami i przykładami, które rozwiązują zagadki asynchroniczności.
Krótka odpowiedź: JavaScript jest jednowątkowy — w danym momencie robi jedną rzecz. Event Loop obsługuje asynchroniczność: opróżnia Call Stack → wykonuje WSZYSTKIE microtasks (Promise.then, await) → wykonuje JEDEN task (setTimeout, event handler) → powtarza. Dlatego Promise.resolve().then(fn) wykonuje się PRZED setTimeout(fn, 0) — microtasks mają wyższy priorytet niż tasks. Długie operacje synchroniczne blokują Event Loop — dla ciężkich obliczeń używaj Web Workers.
JavaScript jest jednowątkowy
To fundament. JavaScript ma jeden główny wątek wykonawczy dla danego kontekstu — w danym momencie Twój kod JS robi jedną rzecz naraz.
Code
// Te operacje wykonują się sekwencyjnieconsole.log('Pierwsza')console.log('Druga')console.log('Trzecia')// Wynik: Pierwsza, Druga, Trzecia — zawsze w tej kolejności
To nie znaczy, że całe środowisko ma fizycznie jeden wątek. Timery, sieć, I/O czy rendering są obsługiwane przez runtime i przeglądarkę poza samym stosem wywołań JavaScript.
Ale jak w takim razie możemy mieć asynchroniczne operacje? Jak przeglądarka obsługuje kliknięcia, fetch, setTimeout — jednocześnie?
Odpowiedź: JavaScript runtime to nie tylko silnik JS.
Microtask Queue — kolejka dla Promise, queueMicrotask, MutationObserver
Event Loop — mechanizm przenoszący zadania z kolejek na stos
W przeglądarce zwykle mówi się o Web APIs. W Node.js analogiczną rolę pełnią timery, I/O i mechanizmy event loop oparte o libuv, więc szczegóły runtime'u trochę się różnią.
Call Stack — stos wywołań
Call Stack działa jak stos talerzy: ostatni włożony = pierwszy zdjęty (LIFO).
1. Call Stack: [console.log('Start')] → wyświetla "Start"
2. Call Stack: [setTimeout] → przekazuje do Web APIs
3. Web APIs: timer 1000ms (działa w tle)
4. Call Stack: [console.log('Koniec')] → wyświetla "Koniec"
5. Call Stack: [] (pusty)
6. ...1000ms mija...
7. Web APIs → callback do Task Queue
8. Event Loop: stack pusty? Tak → przenieś callback
9. Call Stack: [callback] → wyświetla "Timeout"
Wynik: "Start", "Koniec", "Timeout"
Event Loop — serce asynchroniczności
Event Loop to prosty algorytm działający w nieskończonej pętli:
Code
while (true) {
1. Wykonuj kod z Call Stack aż będzie pusty
2. Sprawdź Microtask Queue — wykonaj WSZYSTKIE microtasks
3. Sprawdź Task Queue — wykonaj JEDEN task
4. Runtime może dostać okazję do renderowania lub innych prac systemowych
5. Wróć do kroku 1
}
Kluczowe:
Microtasks mają priorytet nad tasks
Wszystkie microtasks wykonują się przed kolejnym taskiem
Przeglądarka może renderować między taskami, jeśli główny wątek jest wolny
Task Queue vs Microtask Queue
To kluczowa różnica, która często pojawia się na rozmowach.
setTimeout(fn, 0) nie oznacza "wykonaj natychmiast". Oznacza "dodaj do Task Queue najszybciej jak to możliwe". Ale callback musi czekać aż:
Call Stack będzie pusty
Wszystkie microtasks się wykonają
Event Loop przeniesie go na stos
Nie ma gwarancji, że callback wykona się "natychmiast po 0 ms". Przeglądarki mogą opóźniać timery, a w nieaktywnych kartach te opóźnienia bywają dużo większe. Specyfikacja definiuje minimum 4 ms dla odpowiednio zagnieżdżonych timerów.
async/await i Event Loop
async/await to syntactic sugar dla Promise. Kod po await trafia do Microtask Queue:
Code
async function example() { console.log('1 - przed await') await Promise.resolve() console.log('2 - po await')}console.log('3 - start')example()console.log('4 - end')
Przebieg:
Code
1. Synchroniczny: console.log('3 - start') → "3 - start"
2. Wywołaj example()
3. Synchroniczny (w example): console.log('1 - przed await') → "1 - przed await"
4. await → reszta funkcji do Microtask Queue
5. Synchroniczny: console.log('4 - end') → "4 - end"
6. Call Stack pusty → Microtask Queue
7. Microtask: console.log('2 - po await') → "2 - po await"
Wynik: 3 - start, 1 - przed await, 4 - end, 2 - po await
Blocking Event Loop — czego unikać
Ponieważ JavaScript jest jednowątkowy, długie operacje blokują wszystko:
Pytanie 3: Jak sprawić, żeby setTimeout wykonał się przed Promise?
Odpowiedź: W tym samym ticku i przy standardowych mechanizmach planowania microtasks mają priorytet nad tasks. Jeśli oba callbacki są już zakolejkowane, Promise.then() wykona się przed setTimeout(..., 0).
FAQ
Co to jest Event Loop w JavaScript i jak działa?
Event Loop to mechanizm zarządzający asynchronicznością w JavaScript. Działa w pętli: (1) wykonaj cały synchroniczny kod na Call Stack, (2) wykonaj wszystkie microtasks (Promise, await) z Microtask Queue, (3) wykonaj jeden task z Task Queue (setTimeout, event handler), (4) wróć do kroku 1. Dzięki temu JavaScript może obsługiwać asynchroniczne operacje mimo bycia jednowątkowym — ciężka praca (sieć, timery) odbywa się poza stosem wywołań, a wyniki wracają przez kolejki.
Dlaczego Promise wykonuje się przed setTimeout(fn, 0)?
Promise callbacks trafiają do Microtask Queue, a setTimeout callbacks — do Task Queue. Event Loop zawsze opróżnia CAŁĄ Microtask Queue przed wykonaniem kolejnego tasku. Dlatego Promise.resolve().then(() => console.log('A')) wykona się przed setTimeout(() => console.log('B'), 0) — nawet jeśli oba są "asynchroniczne" i oba mają minimalny czas oczekiwania. To najczęstsze pytanie o Event Loop na rozmowach technicznych.
Jaka jest różnica między Task Queue a Microtask Queue?
Task Queue (Callback Queue): trafiają tu callbacki z setTimeout, setInterval, event handlers (click, keydown), I/O. Microtask Queue: trafiają tu Promise.then/catch/finally, queueMicrotask(), MutationObserver, kod po każdym await. Kluczowa różnica: przed każdym nowym taskiem Event Loop opróżnia CAŁĄ Microtask Queue. Jeden task → wszystkie microtasks → następny task. Microtasks zawsze mają priorytet.
Co to jest Call Stack i jak wpływa na wykonanie kodu?
Call Stack to stos wywołań działający na zasadzie LIFO (last in, first out) — ostatnia dodana funkcja jest pierwszą zakończoną. Gdy wywołujesz funkcję, trafia na stos. Gdy kończy wykonanie, jest zdejmowana. Synchroniczny kod wykonuje się na stosie. Event Loop przenosi kolejny task na stos tylko gdy jest on pusty. Dlatego długa operacja synchroniczna (pętla przez 5 sekund) blokuje całą stronę — nic nie może wejść na stos.
Co to jest setTimeout(fn, 0) i dlaczego callback nie wykonuje się natychmiast?
setTimeout(fn, 0) nie znaczy "wykonaj natychmiast" — znaczy "dodaj do Task Queue najszybciej jak to możliwe". Callback musi czekać aż: Call Stack będzie pusty, wszystkie microtasks się wykonają, Event Loop przeniesie go na stos. Przeglądarki mają też minimalny czas opóźnienia (~4ms dla zagnieżdżonych timerów), a w nieaktywnych kartach timery mogą być throttlowane do 1000ms. Używaj setTimeout(fn, 0) do odkładania kodu na następny tick, nie do natychmiastowego wykonania.
Jak unikać blokowania Event Loop w JavaScript?
Długie operacje synchroniczne blokują Event Loop — strona "zamarza". Rozwiązania: (1) podziel pracę na chunki z setTimeout(() => processNextChunk(), 0) — oddajesz kontrolę Event Loop między partiami; (2) użyj Web Workers do ciężkich obliczeń w osobnym wątku (Worker nie blokuje głównego wątku); (3) użyj requestIdleCallback do niskopriorytycznej pracy w czasie bezczynności; (4) operacje I/O (fetch, baza danych) są zawsze asynchroniczne — nie blokują.
Jak async/await wpisuje się w model Event Loop?
async/await to syntactic sugar dla Promise — nie zmienia modelu Event Loop. Kod po await trafia do Microtask Queue, tak jak Promise.then(). Gdy silnik napotka await, zatrzymuje wykonanie funkcji async, odkłada resztę jako microtask i wraca do Call Stack. Praktyczna konsekwencja: const result = await fetch(url) nie blokuje wątku — po await kod czeka w Microtask Queue i wznawia gdy Promise się rozwiąże.
WSZYSTKIE microtasks wykonują się przed kolejnym taskiem
Długie operacje blokują Event Loop
Zrozumienie Event Loop to fundament pisania wydajnego, asynchronicznego JavaScript. Na rozmowie pokaż, że rozumiesz nie tylko "co" się dzieje, ale "dlaczego".
Przygotowujesz się do rozmowy technicznej? Dołóż do tego jeszcze scope, closures, this i prototypy — razem z Event Loop tworzą bardzo częsty zestaw pytań rekrutacyjnych.
Pracuję z tym zawodowo.
Jeśli chcesz przełożyć ten temat na lepszą architekturę frontendu, uporządkować React lub Next.js i podnieść jakość pracy zespołu, skontaktuj się ze mną. Pomagam zamieniać wiedzę z artykułów w praktyczne decyzje technologiczne.
Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.
Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje własne projekty.
Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?
Fachowe porównanie Astro.js i Next.js z perspektywy developera pracującego na co dzień w Next.js. Architektura, wydajność, SEO, DX, koszty i konkretne use case — z benchmarkami i przykładami kodu.
WordPress → Next.js — migracja treści, redirecty 301 i zachowanie pozycji SEO
Jak przenieść stronę z WordPress na Next.js bez utraty pozycji w Google? Eksport treści, mapowanie URL, redirecty 301, migracja obrazów i weryfikacja indeksacji.
Google Search Console + Next.js — indeksacja, błędy, performance i co z nimi robić
Jak korzystać z Google Search Console dla strony Next.js? Weryfikacja, sitemap, indeksacja, Core Web Vitals, crawl budget i najczęstsze problemy — praktyczny poradnik.