Блог за уеб технологии, маркетинг и SEO, мотивация и продуктивност
Какво е Event Loop и защо бавният JavaScript замразява страницата
Event Loop е механизмът в браузъра, който позволява на JavaScript да обработва потребителски действия, мрежови заявки и рендиране - едновременно, въпреки че езикът може да изпълнява само едно нещо в даден момент.
Какво е Event Loop и защо изобщо съществува? Защото JavaScript е еднонишков – може да върши само едно нещо наведнъж. Без Event Loop всяко зареждане на снимка или изчакване на сървър щеше да замразява страницата напълно.
Ако си чел предишните статии от поредицата, вече знаеш как браузърът получава страницата от сървъра, парсва HTML, изгражда DOM, прилага CSS и рендира пикселите чрез Critical Rendering Path. Знаеш и че Reflow и Repaint се задействат при всяка промяна след първоначалното зареждане.
Но остава един въпрос без отговор: кой решава кога и в какъв ред се изпълняват всички тези неща? Кой координира JavaScript кода, потребителските действия, мрежовите заявки и рендирането едновременно, без да се объркат?
Отговорът е Event Loop.

Съдържание на тази страница:
Защо JavaScript е еднонишков?
Преди да разберем Event Loop, трябва да разберем ограничението, което го е наложило.
JavaScript е еднонишков език (single-threaded). Tова означава, че може да изпълнява само едно нещо в даден момент. Има само един Call Stack (стек за извиквания) и той обработва задачите строго последователно.
Това не е случайно. JavaScript е проектиран за браузъра, където манипулира DOM. Ако два JavaScript кода можеха да модифицират DOM едновременно от различни нишки, щеше да е невъзможно да се предвиди крайното състояние на страницата – класическият проблем с race conditions при многонишково програмиране.
Това, че JavaScript е еднонишков решава този проблем елегантно, но създава друг: ако един код се изпълнява дълго, всичко останало чака.
Какво е Call Stack?
Call Stack е структурата, в която JavaScript следи кои функции се изпълняват в момента. Работи на принципа „последен влязъл, първи излязъл“ (LIFO – Last In, First Out).
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
function main() {
const result = square(5);
console.log(result);
}
main();
Call Stack при изпълнение:
[4] multiply(5, 5) ← изпълнява се, връща 25, излиза
[3] square(5) ← изчаква multiply, после излиза
[2] main() ← изчаква square, после излиза
[1] (global) ← входната точка
Когато Call Stack е зает, браузърът не може да прави нищо друго – нито да рендира, нито да реагира на кликове, нито да изпълнява друг код.
Какво е Event Loop?
Event Loop е механизмът, който позволява на JavaScript да е еднонишков и в същото време да обработва асинхронни операции – мрежови заявки, таймери, потребителски действия, без да блокира.
Той прави една проста работа, но я прави непрекъснато:
Докато програмата работи:
1. Ако Call Stack е празен:
→ Вземи следващата задача от Task Queue
→ Постави я в Call Stack
→ Изпълни я
2. Ако Call Stack е зает:
→ Изчакай
Това е буквално целият Event Loop. Той е „пазач“, който наблюдава Call Stack и Task Queue и прехвърля задачи от едното към другото.
Компонентите на Event Loop
За да разбереш как работи Event Loop, трябва да познаваш всички негови части.
Call Stack
Вече го познаваш, тук живее синхронният JavaScript код. Всяка функция влиза, изпълнява се и излиза.
Web APIs (Browser APIs)
Когато JavaScript извика асинхронна операция – setTimeout, fetch, addEventListener – тя се предава на браузъра, не на JavaScript engine. Браузърът има собствени нишки за тези операции и ги изпълнява паралелно, извън Call Stack.
console.log('1');
setTimeout(() => {
console.log('2');
}, 1000);
console.log('3');
Какво се случва:
console.log('1')–>влиза в Call Stack, изпълнява се, излиза.setTimeout(...)–>предава се на Web API (браузъра), не блокира.console.log('3')–>влиза в Call Stack, изпълнява се, излиза.- След 1000ms браузърът поставя callback-а в Task Queue.
- Event Loop вижда празен Call Stack–>взима callback-а–>изпълнява
console.log('2').
Резултат: 1, 3, 2 – не 1, 2, 3.
Task Queue (Macrotask Queue)
Task Queue е опашката, в която чакат macrotasks – задачи, поставени от Web APIs след завършване на асинхронни операции.
Примери за macrotasks:
- Callbacks от
setTimeoutиsetInterval; - Callbacks от I/O операции;
- Callbacks от потребителски събития (
click,keydown,scroll); - Рендиране на страницата.
Event Loop взима по една задача от Task Queue при всеки цикъл.
Microtask Queue
Microtask Queue е специална опашка с по-висок приоритет от Task Queue. Microtasks се изпълняват веднага след текущата задача, преди Event Loop да вземе следващата macrotask.
Примери за microtasks:
- Promise callbacks (
.then(),.catch(),.finally()); async/await(правят кода по-лесен за четене, писане или по-изразителен, без да променят основната функционалност – syntactic sugar върху Promises);queueMicrotask();MutationObservercallbacks.
console.log('1');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
console.log('2');
Резултат: 1, 2, Promise, setTimeout
Защо? Защото след като синхронният код (1 и 2) завърши, Event Loop първо изпразва цялата Microtask Queue (Promise), и едва след това взима следващата macrotask (setTimeout).
Виж задълбоченото MDN ръководство за Microtasks.
Пълният цикъл на Event Loop
Ето точната последователност на един Event Loop цикъл:
1. Изпълни текущата Macrotask (или стартовия скрипт)
↓
2. Изпразни цялата Microtask Queue
(изпълни всички microtasks, включително новодобавени по време на изпълнението)
↓
3. Rendering steps (ако е дошло време):
- Style recalculation
- Layout (Reflow)
- Paint (Repaint)
- Composite
↓
4. Вземи следващата Macrotask от Task Queue
↓
5. Повтори от стъпка 1
Стъпка 3 е ключовата връзка с предишните статии: рендирането се случва между macrotasks, не произволно. Браузърът цели 60 пъти в секунда (веднъж на ~16.67ms) да изпълни rendering steps, но само ако Call Stack е свободен.
Защо JavaScript замразява страницата?
Сега отговорът е очевиден. Ако имаш дълга синхронна операция в Call Stack, тя блокира целия Event Loop, включително rendering steps.
// Симулация на тежка синхронна операция
function heavyTask() {
const start = Date.now();
while (Date.now() - start < 3000) {
// Блокира Call Stack за 3 секунди
}
}
document.getElementById('btn').addEventListener('click', heavyTask);
Докато heavyTask се изпълнява:
- Браузърът не може да рендира–>страницата замръзва визуално.
- Браузърът не може да обработва кликове–>интерфейсът не реагира.
- Браузърът не може да изпълнява други JavaScript задачи.
- Потребителят вижда „замразена“ страница.
Това е точно описанието на „Long Task“ – задача, която заема Call Stack за повече от 50ms. Google счита 50ms за прага, над който потребителят усеща забавяне.
Честите капани в реален код
Синхронна обработка на голям масив
// Блокира за стотици ms при голям масив
const results = largeArray.map(item => heavyTransform(item));
updateUI(results);
// ✅ Разбий на chunks с setTimeout
function processInChunks(array, chunkSize = 100) {
let index = 0;
function processChunk() {
const chunk = array.slice(index, index + chunkSize);
chunk.forEach(item => heavyTransform(item));
index += chunkSize;
if (index < array.length) {
setTimeout(processChunk, 0); // Освобождава Event Loop между chunks
}
}
processChunk();
}
setTimeout(fn, 0) не означава „изпълни веднага“ — означава „постави в Task Queue и изпълни при следващия Event Loop цикъл“. Това дава на браузъра шанс да рендира между chunks.
Promise chain, блокираща Microtask Queue
// Microtask Queue никога не се изпразва → rendering steps никога не се достигат
function infiniteMicrotasks() {
Promise.resolve().then(infiniteMicrotasks);
}
infiniteMicrotasks();
Тъй като Microtask Queue се изпразва напълно преди rendering steps, безкрайна верига от microtasks блокира рендирането толкова ефективно, колкото синхронен цикъл.
Тежки scroll/resize handlers
// Изпълнява се при всеки scroll event — до 60 пъти/сек
window.addEventListener('scroll', () => {
const elements = document.querySelectorAll('.card');
elements.forEach(el => {
// Тежка операция при всеки scroll
el.style.transform = `translateY(${expensiveCalculation()}px)`;
});
});
// ✅ Throttle с requestAnimationFrame
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
updateElements();
ticking = false;
});
ticking = true;
}
});
requestAnimationFrame и Event Loop
requestAnimationFrame (rAF) е специален Web API, проектиран точно за анимации и визуални обновления. За разлика от setTimeout, rAF callbacks се изпълняват непосредствено преди rendering steps — в точния момент, когато браузърът е готов да рендира.
Event Loop цикъл с rAF:
[Macrotask] → [Microtasks] → [rAF callbacks] → [Rendering] → [следваща Macrotask]
Това го прави идеален за анимации:
function animate(timestamp) {
// Изпълнява се преди всеки render frame — максимум 60 пъти/сек
element.style.transform = `translateX(${position}px)`;
position += 2;
if (position < 500) {
requestAnimationFrame(animate); // Планира следващия кадър
}
}
requestAnimationFrame(animate);
За разлика от setInterval(fn, 16), rAF автоматично се синхронизира с refresh rate на монитора и спира когато tab-ът е неактивен – пести ресурси.
Web Workers: истински паралелизъм
Ако имаш наистина тежко изчисление, което не може да се разбие на сегменти (chunks), решението е Web Worker – отделна нишка, която работи паралелно с главната нишка и не блокира Event Loop.
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ data: largeArray }); // Изпрати данни към worker
worker.onmessage = (event) => {
// Получи резултата — това е callback, не блокира
updateUI(event.data.result);
};
// worker.js
self.onmessage = (event) => {
const result = heavyProcessing(event.data.data); // Работи в отделна нишка
self.postMessage({ result });
};
Ограничение: Web Workers нямат достъп до DOM. Те могат само да изчисляват и да комуникират с главната нишка чрез съобщения. DOM манипулацията винаги остава в главната нишка.
async/await и Event Loop
async/await е синтактична захар (syntactic sugar) върху Promises – под капака работи с Microtask Queue.
async function fetchData() {
console.log('1 - преди await');
const data = await fetch('/api/data'); // Предава се на Web API
console.log('3 - след await'); // Това е microtask callback
}
fetchData();
console.log('2 - след извикването');
Резултат: 1, 2, 3
await „паузира“ функцията и я поставя в Microtask Queue докато Promise-ът се резолвира. Останалият синхронен код (2) продължава да се изпълнява веднага.
async/await не прави кода паралелен
Честа грешка:
// ❌ Последователно — бавно (3 секунди общо)
async function loadSequential() {
const users = await fetchUsers(); // Чака 1 сек
const posts = await fetchPosts(); // Чака 1 сек
const comments = await fetchComments(); // Чака 1 сек
}
// ✅ Паралелно — бързо (1 секунда общо)
async function loadParallel() {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
}
Promise.all стартира всички заявки едновременно и изчаква всички да завършат, без да блокира Event Loop.
Погледни и в официалната MDN документация за Event Loop – обяснява Call Stack, Task Queue и връзката между синхронно и асинхронно изпълнение.
Event Loop и рендирането: пълната картина
Сега можем да свържем всичко от поредицата в една цялостна картина:
Браузърът получава HTML от сървъра (HTTP)
↓
HTML Parser изгражда DOM
↓
CSS Parser изгражда CSSOM
↓
DOM + CSSOM → Render Tree → Layout → Paint → Composite
↓
[Страницата е видима]
↓
Event Loop стартира и управлява всичко след това:
┌─────────────────────────────────────────────────┐
│ [JavaScript Task] │
│ → манипулира DOM чрез DOM API │
│ → задейства Reflow/Repaint при нужда │
│ ↓ │
│ [Microtasks — Promise callbacks] │
│ ↓ │
│ [rAF callbacks] │
│ ↓ │
│ [Rendering: Style → Layout → Paint → Composite]│
│ ↓ │
│ [следваща Task от Queue] │
└─────────────────────────────────────────────────┘
Всяко взаимодействие на потребителя – клик, скрол, въвеждане на текст – влиза като Task в Task Queue. Event Loop я взима, изпълнява свързания JavaScript (който може да манипулира DOM чрез DOM API), и накрая браузърът рендира промените.
Как да измериш Long Tasks
Chrome DevTools – Performance таб
В Performance таба Long Tasks се визуализират с червени триъгълници в горния ъгъл на блока. Всяка задача над 50ms е маркирана.
1. DevTools–>Performance–>🔄 (Record and reload).

2. Задействай интеракция (клик, скрол).
3. Спри записа.
4. Търси червени триъгълници в Main нишката.
5.Кликни върху задачата–>Bottom-up таб–>виждаш кой код я причинява.

PerformanceObserver API
// Засича Long Tasks програмно
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
console.warn(`Long Task: ${entry.duration.toFixed(2)}ms`);
});
});
observer.observe({ entryTypes: ['longtask'] });
Lighthouse – Total Blocking Time (TBT)
Total Blocking Time е сумата от всички части на Long Tasks, надхвърлящи 50ms. Lighthouse го показва директно в Performance резултата.
| TBT | Оценка |
|---|---|
| < 200ms | Добро ✅ |
| 200–600ms | Нуждае се от подобрение ⚠️ |
| > 600ms | Лошо ❌ |
Обобщение
Event Loop е механизмът, който позволява на еднонишковия JavaScript да управлява асинхронни операции, потребителски взаимодействия и рендиране едновременно – без race conditions.
Ключови изводи:
- JavaScript е еднонишков – само едно нещо се изпълнява в даден момент.
- Macrotasks (setTimeout, events) се обработват по една на Event Loop цикъл.
- Microtasks (Promises, async/await) се изпълняват веднага след текущата task, преди рендиране.
- Рендирането се случва между macrotasks – дълъг синхронен код го блокира.
- Long Task (> 50ms) замразява интерфейса и е измерима метрика.
requestAnimationFrameсинхронизира кода с rendering cycle.- Web Workers дават истински паралелизъм за тежки изчисления извън главната нишка.
Каква е следващата стъпка?
С тази статия поредицата „Как работи уебът“ би трябвало да затвори пълния кръг – от DNS заявката и IP адреса, през уеб сървъра и HTTP, през HTML parsing, DOM, DOM API, CSSOM, Render Tree, Critical Rendering Path и Reflow/Repaint, до Event Loop, който управлява всичко след зареждането.
Но имам пропуск в публикациите, липсва такава за Render Tree и за CSSOM.
Event Loop е механизмът, който позволява на браузъра да координира изпълнението на JavaScript, обработката на събития и обновяването на интерфейса.
За да разберем напълно как браузърът визуализира страницата, следващата ключова стъпка е да видим как DOM и CSS се комбинират в Render Tree – структурата, която браузърът използва за реалното рисуване на страницата.



