Таймер без фриза с помощью 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, так что выгружать в воркер что-то тяжёлое или важное — вполне себе рабочая тема, о которой не стоит забывать.