1. Теоретические основы многопоточности в браузере
Теоретические основы многопоточности в браузере
Представьте, что вы открываете тяжёлый дашборд в браузере: десятки графиков, фильтрация миллионов строк таблицы, построение отчётов в реальном времени. В какой-то момент интерфейс замирает — кнопки не реагируют, скролл заедает, а курсор превращается в спиннер. Это происходит не потому, что браузер «сломался», а потому что JavaScript выполняет вычисления в том же потоке, который отвечает за отрисовку интерфейса. Именно эту фундаментальную проблему решают Web Workers.
Почему JavaScript однопоточен
JavaScript изначально проектировался как язык для браузера — для обработки кликов, валидации форм и манипуляций с DOM. Для таких задач многопоточность была бы избыточной и опасной: если два потока одновременно изменяют один и тот же DOM-узел, возникает состояние гонки (race condition), результат которого непредсказуем. Поэтому архитекторы языка выбрали модель однопоточного выполнения с асинхронным event loop.
Однопоточность означает, что в любой момент времени выполняется ровно одна инструкция JavaScript. Это не слабость — это гарантия: разработчику не нужно беспокоиться о блокировках, мьютексах и deadlock'ах. Но за эту простоту приходится платить: любая тяжёлая операция блокирует весь поток.
Event Loop: как браузер распоряжается единственным потоком
Механизм Event Loop — это сердце асинхронного выполнения в JavaScript. Он работает по простому алгоритму:
setTimeout, обработчики событий, ответы от fetch.MutationObserver.Event Loop бесконечно проверяет: если Call Stack пуст — забирает задачу из очереди и выполняет её. Если стек не пуст — ждёт.
> Ключевое правило: браузер не может отрисовать кадр, пока Call Stack не опустеет. Если JavaScript выполняет тяжёлый цикл на 500 мс — браузер не обновит экран эти 500 мс. Пользователь видит фриз.
Вот наглядная модель взаимодействия:
| Компонент | Роль | Пример |
|-----------|------|--------|
| Call Stack | Выполняет синхронный код | for-цикл с вычислениями |
| Web APIs | Асинхронные операции браузера | setTimeout, fetch, DOM-события |
| Task Queue | Очередь колбэков готовых к выполнению | Обработчик клика |
| Microtask Queue | Приоритетная очередь промисов | .then(), queueMicrotask() |
| Event Loop | Координирует переключение между очередями | Проверяет стек → забирает задачу |
Проблема блокировки UI на практике
Рассмотрим конкретный сценарий. Допустим, вы обрабатываете CSV-файл с 500 000 строк в браузере:
Этот цикл может занять 2–3 секунды. Всё это время Event Loop занят: браузер не может обработать ни один клик, не обновит анимацию, не перерисует скролл. Пользователь видит «замёрзший» интерфейс.
Можно ли обойтись без воркеров? Частично — да. Разбивка на чанки через setTimeout или requestIdleCallback позволяет «отдавать управление» браузеру между итерациями:
Но у этого подхода три серьёзных недостатка:
setTimeout добавляет задержку минимум 4 мс.Многопоточность через Web Workers
Web Worker — это отдельный поток выполнения JavaScript, который работает параллельно с основным потоком (main thread). Он имеет собственный Event Loop, собственную кучу памяти и собственный контекст выполнения.
Главные ограничения, которые накладывает эта архитектура:
window — вместо него используется контекст WorkerGlobalScope.Эти ограничения — не недостатки, а осознанный архитектурный выбор. Они гарантируют отсутствие состояний гонки при работе с DOM и делают модель предсказуемой.
Когда воркеры действительно нужны
Не каждая асинхронная операция требует воркера. Вот ориентир для принятия решения:
| Операция | Подход | Причина |
|----------|--------|---------|
| Запрос к API | fetch / async/await | I/O-операция, не блокирует CPU |
| Таймер | setTimeout / setInterval | Лёгкая отложенная задача |
| Парсинг большого JSON | Web Worker | CPU-bound, блокирует поток |
| Шифрование данных | Web Worker | Тяжёлое CPU-вычисление |
| Обработка изображений | Web Worker | Пиксельные манипуляции |
| Фильтрация 100K+ строк | Web Worker | Алгоритмическая сложность |
| Обновление DOM | Main thread | Доступ только из основного потока |
Правило простое: если операция CPU-bound (требует много вычислений процессора) и не нуждается в DOM — она кандидат на вынос в воркер. Если операция I/O-bound (ожидание сети, диска) — достаточно стандартных асинхронных API.
Модель памяти: изоляция как гарантия безопасности
Каждый Web Worker работает в собственном изолированном пространстве памяти. Это фундаментальное отличие от многопоточности в Java или C++, где потоки разделяют общую память.
В модели JavaScript потоки не разделяют память — они обмениваются копиями данных (или передают владение через transferable objects). Это исключает целый класс ошибок, связанных с конкурентным доступом, но требует осознанного подхода к передаче данных — тема, которую мы детально разберём в третьей статье курса.
> Аналогия: представьте двух программистов в разных офисах. Они не могут сидеть за одним компьютером (общая память), но могут отправлять друг другу документы по почте (сообщения). Это медленнее, чем работать вместе, но зато никто не мешает друг другу.
Такая модель — основа архитектуры Actor Model, которая используется в Erlang, Akka и других системах, доказавших свою надёжность в высоконагруженных приложениях.