JavaScript — однопоточный язык: он выполняет только одну задачу за раз. Но браузер не зависает, пока ждёт ответа сервера, и анимации не останавливаются во время обработки данных. Всё это возможно благодаря Event Loop.
Когда вызывается функция, она помещается на вершину стека. Когда функция завершается, она удаляется из стека. JavaScript выполняет код только тогда, когда стек пуст или содержит текущую функцию.
Если стек занят долгой операцией (тяжёлые вычисления, бесконечный цикл), браузер не может обрабатывать события и перерисовывать страницу. Интерфейс «замерзает».
Heap — область памяти, где хранятся объекты и замыкания. В отличие от стека, который автоматически управляется, в Heap данные живут пока на них есть ссылки. Garbage Collector автоматически освобождает память, когда ссылок не остаётся.
Macrotask Queue (очередь макрозадач) — обычные задачи, ожидающие выполнения:
click, keydown)setTimeout, setInterval)Microtask Queue (очередь микрозадач) — задачи с высшим приоритетом:
.then, .catch, .finally)async/await (каждый await — микрозадача)queueMicrotask()1. Выполнить весь синхронный код (стек очищается)
2. Выполнить все микрозадачи из Microtask Queue (до конца)
3. Браузер перерисовывает страницу (если нужно)
4. Взять одну макрозадачу из Macrotask Queue и выполнить
5. Перейти к шагу 2
Ключевое правило: микрозадачи всегда выполняются до следующей макрозадачи. Promise.then всегда опережает setTimeout.
Если синхронный код занимает стек более ~16мс, браузер пропускает кадр анимации. При 100мс задержке ощущается как лагание. При 300мс+ — явное зависание.
// Этот код заблокирует UI на несколько секунд
function heavyCalculation() {
let sum = 0
for (let i = 0; i < 1_000_000_000; i++) {
sum += i
}
return sum
}setTimeout(fn, 0) — перенести задачу в Macrotask Queue, дав браузеру возможность перерисоваться между порциями работы.
requestAnimationFrame — выполнить код перед следующей перерисовкой.
Web Workers — выполнить тяжёлые вычисления в отдельном потоке. Worker не имеет доступа к DOM, но общается с основным потоком через сообщения. Идеально для: шифрования, обработки изображений, парсинга больших данных, алгоритмов сортировки.
Синхронный код → Микрозадачи → Перерисовка → Макрозадача → Микрозадачи → ...Понимание этого порядка объясняет, почему Promise.resolve().then() выполняется раньше setTimeout(fn, 0).
Порядок выполнения микро- и макрозадач, разбивка тяжёлого цикла
// Демонстрация порядка выполнения
console.log('1: Синхронный код начало')
setTimeout(() => console.log('4: setTimeout (макрозадача)'), 0)
Promise.resolve()
.then(() => console.log('3: Promise.then (микрозадача)'))
.then(() => console.log('3b: Цепочка Promise (тоже микрозадача)'))
console.log('2: Синхронный код конец')
// Порядок вывода: 1 → 2 → 3 → 3b → 4
// Разбиваем тяжёлый цикл на асинхронные порции
// Это позволяет браузеру перерисовывать UI между порциями
function processInChunks(items, chunkSize, processFn) {
return new Promise((resolve) => {
let index = 0
function processChunk() {
const start = performance.now()
// Обрабатываем порцию, пока не истекло время кадра (~16мс)
while (index < items.length && (performance.now() - start) < 4) {
processFn(items[index], index)
index++
}
if (index < items.length) {
// Отдаём управление браузеру через setTimeout
// Браузер может перерисовать страницу между вызовами
setTimeout(processChunk, 0)
} else {
resolve() // всё обработано
}
}
processChunk()
})
}
// Использование: обрабатываем 100 000 элементов без заморозки UI
const bigArray = Array.from({ length: 100_000 }, (_, i) => i)
await processInChunks(bigArray, 1000, (item) => {
// какая-то обработка элемента
})
console.log('Обработка завершена без заморозки UI')JavaScript — однопоточный язык: он выполняет только одну задачу за раз. Но браузер не зависает, пока ждёт ответа сервера, и анимации не останавливаются во время обработки данных. Всё это возможно благодаря Event Loop.
Когда вызывается функция, она помещается на вершину стека. Когда функция завершается, она удаляется из стека. JavaScript выполняет код только тогда, когда стек пуст или содержит текущую функцию.
Если стек занят долгой операцией (тяжёлые вычисления, бесконечный цикл), браузер не может обрабатывать события и перерисовывать страницу. Интерфейс «замерзает».
Heap — область памяти, где хранятся объекты и замыкания. В отличие от стека, который автоматически управляется, в Heap данные живут пока на них есть ссылки. Garbage Collector автоматически освобождает память, когда ссылок не остаётся.
Macrotask Queue (очередь макрозадач) — обычные задачи, ожидающие выполнения:
click, keydown)setTimeout, setInterval)Microtask Queue (очередь микрозадач) — задачи с высшим приоритетом:
.then, .catch, .finally)async/await (каждый await — микрозадача)queueMicrotask()1. Выполнить весь синхронный код (стек очищается)
2. Выполнить все микрозадачи из Microtask Queue (до конца)
3. Браузер перерисовывает страницу (если нужно)
4. Взять одну макрозадачу из Macrotask Queue и выполнить
5. Перейти к шагу 2
Ключевое правило: микрозадачи всегда выполняются до следующей макрозадачи. Promise.then всегда опережает setTimeout.
Если синхронный код занимает стек более ~16мс, браузер пропускает кадр анимации. При 100мс задержке ощущается как лагание. При 300мс+ — явное зависание.
// Этот код заблокирует UI на несколько секунд
function heavyCalculation() {
let sum = 0
for (let i = 0; i < 1_000_000_000; i++) {
sum += i
}
return sum
}setTimeout(fn, 0) — перенести задачу в Macrotask Queue, дав браузеру возможность перерисоваться между порциями работы.
requestAnimationFrame — выполнить код перед следующей перерисовкой.
Web Workers — выполнить тяжёлые вычисления в отдельном потоке. Worker не имеет доступа к DOM, но общается с основным потоком через сообщения. Идеально для: шифрования, обработки изображений, парсинга больших данных, алгоритмов сортировки.
Синхронный код → Микрозадачи → Перерисовка → Макрозадача → Микрозадачи → ...Понимание этого порядка объясняет, почему Promise.resolve().then() выполняется раньше setTimeout(fn, 0).
Порядок выполнения микро- и макрозадач, разбивка тяжёлого цикла
// Демонстрация порядка выполнения
console.log('1: Синхронный код начало')
setTimeout(() => console.log('4: setTimeout (макрозадача)'), 0)
Promise.resolve()
.then(() => console.log('3: Promise.then (микрозадача)'))
.then(() => console.log('3b: Цепочка Promise (тоже микрозадача)'))
console.log('2: Синхронный код конец')
// Порядок вывода: 1 → 2 → 3 → 3b → 4
// Разбиваем тяжёлый цикл на асинхронные порции
// Это позволяет браузеру перерисовывать UI между порциями
function processInChunks(items, chunkSize, processFn) {
return new Promise((resolve) => {
let index = 0
function processChunk() {
const start = performance.now()
// Обрабатываем порцию, пока не истекло время кадра (~16мс)
while (index < items.length && (performance.now() - start) < 4) {
processFn(items[index], index)
index++
}
if (index < items.length) {
// Отдаём управление браузеру через setTimeout
// Браузер может перерисовать страницу между вызовами
setTimeout(processChunk, 0)
} else {
resolve() // всё обработано
}
}
processChunk()
})
}
// Использование: обрабатываем 100 000 элементов без заморозки UI
const bigArray = Array.from({ length: 100_000 }, (_, i) => i)
await processInChunks(bigArray, 1000, (item) => {
// какая-то обработка элемента
})
console.log('Обработка завершена без заморозки UI')Напиши функцию runWithoutBlocking(count, callback), которая выполняет callback count раз, но разбивает работу на порции по 1000 итераций с паузой через setTimeout между ними. Функция должна возвращать Promise, который resolves когда все итерации выполнены.
CHUNK_SIZE — 1000. Следующую порцию запускай через setTimeout(runChunk, 0). Когда completed достигает count, вызови resolve(). Math.min(completed + CHUNK_SIZE, count) даёт правильный конец порции.