← Браузер/Event Loop в браузере#178 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: async и сетьТермин: Event LoopТермин: Core Web Vitals

Event Loop в браузере

JavaScript — однопоточный язык: он выполняет только одну задачу за раз. Но браузер не зависает, пока ждёт ответа сервера, и анимации не останавливаются во время обработки данных. Всё это возможно благодаря Event Loop.

Стек вызовов (Call Stack)

Когда вызывается функция, она помещается на вершину стека. Когда функция завершается, она удаляется из стека. JavaScript выполняет код только тогда, когда стек пуст или содержит текущую функцию.

Если стек занят долгой операцией (тяжёлые вычисления, бесконечный цикл), браузер не может обрабатывать события и перерисовывать страницу. Интерфейс «замерзает».

Heap

Heap — область памяти, где хранятся объекты и замыкания. В отличие от стека, который автоматически управляется, в Heap данные живут пока на них есть ссылки. Garbage Collector автоматически освобождает память, когда ссылок не остаётся.

Очереди задач

Macrotask Queue (очередь макрозадач) — обычные задачи, ожидающие выполнения:

  • Обработчики событий (click, keydown)
  • Таймеры (setTimeout, setInterval)
  • Колбэки I/O, XHR
  • MessageChannel, postMessage
  • Microtask Queue (очередь микрозадач) — задачи с высшим приоритетом:

  • Колбэки Promise (.then, .catch, .finally)
  • async/await (каждый await — микрозадача)
  • queueMicrotask()
  • MutationObserver
  • Как работает Event Loop

    1. Выполнить весь синхронный код (стек очищается)

    2. Выполнить все микрозадачи из Microtask Queue (до конца)

    3. Браузер перерисовывает страницу (если нужно)

    4. Взять одну макрозадачу из Macrotask Queue и выполнить

    5. Перейти к шагу 2

    Ключевое правило: микрозадачи всегда выполняются до следующей макрозадачи. Promise.then всегда опережает setTimeout.

    Почему UI замерзает

    Если синхронный код занимает стек более ~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')

    Event Loop в браузере

    JavaScript — однопоточный язык: он выполняет только одну задачу за раз. Но браузер не зависает, пока ждёт ответа сервера, и анимации не останавливаются во время обработки данных. Всё это возможно благодаря Event Loop.

    Стек вызовов (Call Stack)

    Когда вызывается функция, она помещается на вершину стека. Когда функция завершается, она удаляется из стека. JavaScript выполняет код только тогда, когда стек пуст или содержит текущую функцию.

    Если стек занят долгой операцией (тяжёлые вычисления, бесконечный цикл), браузер не может обрабатывать события и перерисовывать страницу. Интерфейс «замерзает».

    Heap

    Heap — область памяти, где хранятся объекты и замыкания. В отличие от стека, который автоматически управляется, в Heap данные живут пока на них есть ссылки. Garbage Collector автоматически освобождает память, когда ссылок не остаётся.

    Очереди задач

    Macrotask Queue (очередь макрозадач) — обычные задачи, ожидающие выполнения:

  • Обработчики событий (click, keydown)
  • Таймеры (setTimeout, setInterval)
  • Колбэки I/O, XHR
  • MessageChannel, postMessage
  • Microtask Queue (очередь микрозадач) — задачи с высшим приоритетом:

  • Колбэки Promise (.then, .catch, .finally)
  • async/await (каждый await — микрозадача)
  • queueMicrotask()
  • MutationObserver
  • Как работает Event Loop

    1. Выполнить весь синхронный код (стек очищается)

    2. Выполнить все микрозадачи из Microtask Queue (до конца)

    3. Браузер перерисовывает страницу (если нужно)

    4. Взять одну макрозадачу из Macrotask Queue и выполнить

    5. Перейти к шагу 2

    Ключевое правило: микрозадачи всегда выполняются до следующей макрозадачи. Promise.then всегда опережает setTimeout.

    Почему UI замерзает

    Если синхронный код занимает стек более ~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) даёт правильный конец порции.

    Загружаем среду выполнения...
    Загружаем AI-помощника...