Как выполнять JavaScript код в реальном времени без блокировки браузера

Проблема с точностью таймеров в музыкальном приложении

Я разрабатываю простой музыкальный секвенсор на JavaScript. Для воспроизведения звуков использую аудиобиблиотеку.

Быстро понял, что setTimeout и setInterval не подходят для точной синхронизации. Их погрешность слишком большая для музыкальных целей.

Мой текущий подход

Вместо таймеров пробую такой метод:

  1. Создаю очередь музыкальных событий (массив с нотами и временными метками)
  2. Обрабатываю очередь в цикле while

Вот пример кода:

// Очередь событий: время и нота
var events = [[0, 'До'], [300, 'Ре'], [600, 'Ми']]

var startTime = Date.now()

while(events.length > 0) {
    var currentTime = Date.now()
    var triggerTime = events[0][0] + startTime
    
    if(currentTime >= triggerTime) {
        soundPlay(events[0][1]) // Играем ноту
        events.shift()         // Удаляем событие
    }
}

В чем проблема

Этот метод дает нужную точность, но полностью блокирует выполнение остального JavaScript кода. Пока играет последовательность, весь остальной код замирает.

Результат: тишина во время выполнения цикла, а потом все звуки воспроизводятся одновременно.

Пробовал выносить цикл в отдельную функцию и вызывать через setTimeout, но это не решает проблему блокировки.

Вопрос: Как обработать очередь событий с точной синхронизацией, не останавливая при этом выполнение остального кода?

Используй Web Audio API - он для этого и создан. AudioContext.currentTime точнее Date.now(), а звуки можно планировать заранее через audioNode.start(whenTime). Если нужен чистый JS, то requestAnimationFrame + небольшой буфер событий лучше setTimeout. Обрабатывай события порциями, а не все сразу.

Разбей цикл на мелкие кусочки через setTimeout(func, 0). Вместо while сделай функцию, которая проверяет одно событие из очереди и вызывает себя через setTimeout. Получишь псевдо-асинхронность — браузер успеет обработать другие задачи между итерациями. У меня работало в игре с точным таймингом для анимаций. Погрешность будет чуть больше, чем в твоем методе, зато без блокировки.

Web workers спасут тебя! Выноси всю логику очереди в отдельный worker - там можешь крутить бесконечные циклы без тормозов основного потока. Команды на воспроизведение кидай через postMessage обратно. Звуки всё равно играть придётся на основном потоке, но расчёт времени будет работать отдельно.