Таймер без фриза с помощью Web Worker


Если в JS запустить таймер с помощью setInterval, который срабатывает довольно часто, например, раз в 100мс, а затем уйти из таба на некоторое время, то окажется, что браузер остановит таймер на время «неактивности» таба. «Фриз» таймера в неактивном табе можно посмотреть тут https://codesandbox.io/p/sandbox/dreamy-bartik-vkkmjr.

Это не баг, а фича: с целью экономии ресурсов браузеры троттлят фоновую активность тех табов, которые сейчас не открыты.

Но как быть, если нужно, чтобы таймер в фоновом табе гарантированно работал без замедления и остановки? На помощь приходит веб воркер, который работает в браузере отдельном потоке и поэтому на его работу оптимизации основного потока не распространяются.

Вот пример таймера в отдельном файле воркера, к примеру, timerWorker.ts:

let timerId: number | null = null;
self.onmessage = (event: MessageEvent) => {
const { data } = event;
switch (data) {
case "start":
timerId = self.setInterval(() => {
self.postMessage("tick");
}, 100);
break;
case "stop":
if (timerId) {
clearInterval(timerId);
timerId = null;
}
break;
}
};

В воркере нет объекта window, но есть self, местный аналог https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/self. С основным приложением воркер общается с помощью команды postMessage. При этом из приложения также можно дёргать postMessage воркера и подписываться на событие onmessage:

function App() {
const [time, setTime] = useState<number>(0);
const workerRef = useRef<Worker | null>(null);
useEffect(() => {
workerRef.current = new Worker("/timerWorker.ts");
workerRef.current.postMessage("start");
workerRef.current.onmessage = (event) => {
if (event.data === "tick") {
setTime((prevTime) => prevTime + 1);
}
};
return () => {
workerRef.current?.postMessage("stop");
};
}, []);
return <div>{time}</div>;
}

В воркере, к сожалению, нет доступа к DOM-у, но зато доступны разные API, например, Fetch или Canvas, так что выгружать в воркер что-то тяжёлое или важное — вполне себе рабочая тема, о которой не стоит забывать.