Kawałek Kodu

Żyjąc w pośpiechu chcemy, aby wszystko co nas otacza dotrzymywało nam tempa. Kurier nie dostarczył paczki, a weekend tuż tuż? Kiepsko. Współbiegacz jest kilka ulic za nami? Okropieństwo. SetInterval zacina się, choć powinien działać błyskawicznie? Tragedia!

Zachwyt i rozczarowanie.

Mamy dwie możliwości wykonywania cyklicznych operacji. Albo używamy setInterval (ewentualnie setTimeout w pętli) albo requestAnimationFrame. I wszystko jest ok dopóki nasz skrypt wykorzystujący jedną z opcji działa w aktywnej zakładce. W momencie kiedy przełączymy się na inną zakładkę, skrypt zostaje wstrzymany albo zwalnia. A ściśle mówiąc, jeśli mamy do czynienia z requestAnimationFrame, to metoda zostanie spauzowana w większości przeglądarek, a w przypadku setInterval czy setTimeout, interwał poniżej 1s zostanie "zaokrąglony" do 1s. Chcesz się upewnić? Proszę bardzo:

let counter = 1;
const startTime = Date.now();

const timer = setInterval(function() {
  if (counter === 20) {
    clearInterval(timer);
    document.querySelector("i").innerText = (Date.now() - startTime) / 1000;
  }
  document.querySelector("span").innerText = counter++;
}, 500);

W tym krótkiem skrypcie odliczamy co pół sekundy do 20. Wartość licznika umieszczona jest w elemencie SPAN. 20-tkę licznik powinien więc osiągnąć w 10 sekund. I tak jest, kiedy zakładka jest aktywna. Zresztą czas działania zobaczysz w elemencie I. Klawo!
Spróbuj jednak uruchomić skrypt i natychmiast przełączyć się na inną zakładkę. Co zobaczysz po 10 sekundach kiedy wrócisz do zakładki? Skrypt nadal odlicza. A kiedy skończy? Skończy po 20 sekundach. Sprawdź to jeszcze raz uruchamiając skrypt, przełączając się na inną zakładkę, tym razem odczekując osławione 20 sekund. Smutek!

Jak cieszyć się w pełni setInterval?

Z pomocą przyjdzie nam robol zwany Worker'em. Pozwala on uruchomić skrypt JS jako odrębny wątek. I pomimo przełączenia się na inną zakładkę nasz Worker wykonuje skrypt pełną parą. Skoro tak, to trzeba przenieść setInterval właśnie do niego. Ale jak Worker skomunikuje się z głównym skryptem i da znać przy każdym wywołaniu co określony czas? Od tego mamy metodę postMessage, która pozwala wysyłać informacje z głównego lub wątku Workera oraz zdarzenie onMessage, które pozwala je odbierać. W naszym przypadku nie musimy wysyłać konkretnej informacji, może być pusta, po to tylko aby wywołać zdarzenie onMessage w głównym wątku, które będzie nam symulować setInterval (który byłby standardowo umieszczony właśnie w tym wątku).

Worker będzie wyglądać tak:

setInterval(function() {
  postMessage("");
}, 500);

a kod głównego wątku:

let counter = 1;
const startTime = Date.now();

/* inicjujemy Worker */
const worker = new Worker("setinterval-licznik-worker.js");
/* czekamy na wiadomość zwrotną
   z postMessage wysyłaną przez
   kod Worker'a
*/
worker.addEventListener("message", function() {
  if (counter === 20) {
/* przerywamy działanie Worker'a */
    worker.terminate();
    document.querySelector("i").innerText =
      (Date.now() - startTime) / 1000;
  }
  document.querySelector("span").innerText = counter++;
});

Tym razem, niezależnie od tego czy zakładka jest aktywna czy nie, powinieneś otrzymać zbliżony do 10s czas wykonania skryptu.

 

Przydatne linki:
Web Workers
Obcinanie interwału setInterval
Licznik z wpisu oparty o setInterval
Licznik z wpisu oparty o Worker