Суть Читай реальные сниппеты воркера, postMessage и service worker — предскажи поведение во время выполнения или баг, потом выбери фикс с наибольшим рычагом, который сеньор делает первым.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Баги воркеров прячутся на границе сообщений и в обработчике жизненного цикла, а не в алгоритме. Прочитай каждый сниппет, предскажи, что реально происходит на границе, и выбери фикс прежде, чем тянуться к профайлеру.
Цель
Отработай цикл, который ты гоняешь в каждом инциденте с воркером: прочитать вызов postMessage или обработчик жизненного цикла, найти скрытый clone-налог, или детачнутый буфер, или забытый waitUntil, и взять фикс с наибольшим рычагом.
Сниппет 1 — клон, который не переместился
// main.js — выносим тяжёлый фильтр изображения в воркерconst worker = new Worker('filter.js', { type: 'module' });const pixels = new Uint8ClampedArray(width * height * 4); // ~32 МБworker.postMessage({ pixels, width, height }); // (A)applyOtherUiWork(); // (B) выполняется на ~32 мс позже
Викторина
Completed
Строка (B) выполняется примерно на 32 мс позже ожидаемого, хотя фильтр идёт в воркере. Что происходит на строке (A) и какой однострочный фикс?
Heads-up Старт воркера идёт вне вызывающего потока и не блокирует строку (B). Блокировка — это синхронный structured clone буфера на 32 МБ на главном потоке.
Heads-up Строка (B) ничего не await — она выполняется сразу после возврата postMessage. Задержка — синхронный клон внутри postMessage, а не ответ воркера.
Heads-up Это полностью избежимо. Передай буфер в transfer-списке, и копия станет O(1)-передачей владения; ~32 мс исчезают.
Сниппет 2 — буфер, использованный после transfer
// main.jsconst buf = new Float32Array(1_000_000);fillSamples(buf);worker.postMessage(buf, [buf.buffer]); // передан через transferconst sum = buf.reduce((a, x) => a + x); // (C) снова читает buf
Викторина
Completed
Что происходит на строке (C) и что это говорит о семантике transfer?
Heads-up Transfer — противоположность копии. Он передаёт память приёмнику и детачит вью отправителя. Второй рабочей копии у отправителя нет.
Heads-up Здесь нет разделяемой памяти — transfer это односторонняя передача владения, а не живой общий вью. Буфер отправителя просто детачнут и пуст.
Heads-up reduce выполняется синхронно на вызывающем потоке. Суть в том, что читаемый им буфер детачнут, а не в переносе вычисления.
Под нагрузкой этот service worker иногда активируется с пустым или наполовину заполненным кэшем. В чём дефект и фикс?
Heads-up addAll отклоняется целиком, если хоть один запрос упал; частичное наполнение здесь — от неподождавшегося промиса, а не от addAll. Обёртка в event.waitUntil держит воркер живым до завершения.
Heads-up Версионные имена — это как раз способ избежать коллизий, и shell-v3 здесь ничто не удаляет. Баг — пропущенный waitUntil, дающий воркеру продвинуться рано.
Heads-up Абсолютные same-origin пути кэшируются нормально. Недетерминизм — это неподождавшийся промис, чинится event.waitUntil.
Воркер рендерит нормально, но строки (D)–(E) на главном потоке ведут себя неправильно. Что происходит и почему?
Heads-up Платформа запрещает двум потокам гонять один bitmap. transferControlToOffscreen делает владение эксклюзивным — главный поток больше не может рисовать в этот canvas.
Heads-up Механизма освобождения-и-возврата нет. Transfer постоянен на время жизни canvas; контекст главного потока просто недоступен.
Heads-up После transferControlToOffscreen у элемента нет своего контекста рендеринга — bitmap'ом владеет воркер. Понадобится отдельный элемент canvas.
Итог
Каждый инцидент с воркером читается на границе, а не в алгоритме: postMessage крупного буфера по значению блокирует отправителя на синхронном structured clone — передавай ArrayBuffer через transfer; переданный буфер детачнут у отправителя — не читай его после (используй SharedArrayBuffer, если данные нужны обоим); install у service worker должен оборачивать асинхронное наполнение кэша в event.waitUntil, иначе воркер продвинется или умрёт рано; а transferControlToOffscreen делает владение canvas эксклюзивным, так что главный поток больше не рисует. Сначала читай обработчики сообщений и жизненного цикла — там и стоимость, и баги.