Tips

JavaScriptのWorkerで重い処理を同時に実行する方法

  • このエントリーをはてなブックマークに追加

JavaScriptにはWorkerという仕組みがあり、処理の一部を分散させて実行スピードを遅くさせないことができます。

初学者向けの教材やスクールでは習わない人が多いようですが、使い方を覚えると便利ですしPWAのWebサイトを作成する際には必須の知識になります。

PWAはスマホアプリのようにホーム画面にオリジナルのアイコンでサイトをインストールさせる考え方で、WebサイトをPWAにすることが最近のトレンドにもなっているので初学者の方でも覚えておくと良いでしょう。

動画でも解説しているので動画の方がやりやすい方はこちらからどうぞ。

JavaScriptのWorkerが必要な例

まずHTMLで以下のようなページを作ったと想定します。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link rel="stylesheet" href="./style.css" />
    <script src="./script.js" defer></script>
  </head>
  <body>
    <div class="wrap">
      <p id="result"></p>
      <button id="btn">計算する</button>
    </div>
    <input type="text" placeholder="入力してください" />
  </body>
</html>

「計算する」をクリックするとJS側で計算を実行するもので、計算の処理については以下のようにします。

const result = document.querySelector("#result");
const btn = document.querySelector("#btn");

btn.addEventListener("click", () => {
  let total = 0;
  for (let i = 1; i < 2000000000; i++) {
    total += 1;
  }
  result.textContent = total;
});

「計算する」をクリックするとJS側で計算が実行され、計算が完了すると画面に計算結果が表示されます。

機能としてはシンプルなのですが、計算の数が2000000000回と膨大なため「計算する」をクリックしてから計算結果が表示されるまで若干のタイムラグがあります。

これくらいは許容できるのですが加えて問題なのが、「計算する」をクリックして計算結果を待っている時に入力欄が入力できなくなることです。

計算が膨大なため一時的にブラウザが固まっているためです。

JavaScriptではWorkerを使って作り替えることができます。

JavaScriptのWorkerの作り方

まずWorkerは別途ファイルを分けておく必要があり、今回はメインで記述するファイルをscript.jsとしてWorkerとして記述するファイルをworker.jsという名前で作成します。

さらにWorkerはJavaScriptの組み込みクラスなので、script.js側でインスタンス化することでWorkerとして使用できるようになります。

インスタンス化する際の引数は先ほど分けたWorkerを記述するworker.jsを指定するルールになります。

const result = document.querySelector("#result");
const btn = document.querySelector("#btn");

// ここから修正
const worker = new Worker("worker.js");
console.log(worker);

インスタンス化に成功しているとコンソールログに上図のようなものが出力されます。

現在script.jsとworker.jsと2つのファイルに分けていますが関係性としては以下のような形になります。

①script.jsから計算の初期値をworker.jsに送信
②worker.jsで①を受信し計算を実行
③worker.jsで実行した②の結果をscript.jsに送信
④script.jsで③をworker.jsから受信して後続の処理へ

実際のコードを通して解説していきます。

①script.jsから計算の初期値をworker.jsに送信

画面上の動作としては「計算する」というボタンをクリックすると計算が始まる想定なので、ボタンに対してaddEventListenerのクリックイベントの中でpostMessageを実行します。

postMessageはWorkerのメソッドでWorkerをインスタンス化していると使用することができます。

引数にはworker.jsに送信したいものを指定することになっていて、今回の計算の初期値である「0」を入れておきます。

const result = document.querySelector("#result");
const btn = document.querySelector("#btn");

const worker = new Worker("worker.js");
// ここから追加
btn.addEventListener("click", () => {
  worker.postMessage(0);
});

②worker.jsで①を受信し計算を実行

script.js側で送信したものをworker.js側で受信するには、worker.jsでonmessageという関数を使用します。

onmessageは引数にeventを取ると、e.dataというプロパティにアクセスすることができてe.dataに①で送信したものが入っているという仕組みです。

onmessage = (e) => {
  console.log(e.data); // 0が入っている
};

計算の初期値である「0」を受け取れたので、冒頭でやったような計算をこのままworker.jsの中で実行していきます。

onmessage = (e) => {
  console.log(e.data);
  // ここから追加
  let total = e.data;
  for (let i = 1; i < 2000000000; i++) {
    total += 1;
  }
};

③worker.jsで実行した②の結果をscript.jsに送信

②で計算まで完了しましたが現状だと計算結果はworker.jsの中にしかないのでscript.jsに送信して戻すことになります。

データの送信は①でも使ったpostMessageを使うだけです。

onmessage = (e) => {
  let total = e.data;
  for (let i = 1; i < 2000000000; i++) {
    total += 1;
  }
  // ここを追加
  postMessage(total);
};

④script.jsで③をworker.jsから受信して後続の処理へ

worker.jsから計算結果が返ってきたので、その値を使ってscript.jsの処理に使用できます。

今回は計算結果をブラウザの画面に表示することにします。

const result = document.querySelector("#result");
const btn = document.querySelector("#btn");

const worker = new Worker("worker.js");
btn.addEventListener("click", () => {
  worker.postMessage(0);
});

// ここから追加
worker.onmessage = function (e) {
  result.textContent = e.data;
};

計算結果や表示されるスピードは冒頭の内容と同じなのですが、「計算する」をクリックして計算している最中でも入力欄を操作することができていますね。

このように処理の重たいコードがあってブラウザが固まってしまう場合に、Workerを使うことでユーザーの操作性を改善することができます。

2つのファイルを行き来するので最初は戸惑うのですがコンソールログを挟みながら順番を追っていくとわかりやすいです。

const result = document.querySelector("#result");
const btn = document.querySelector("#btn");

const worker = new Worker("worker.js");

btn.addEventListener("click", () => {
  worker.postMessage(0);
  // ここを追加
  console.log("ワーカーへ依頼");
});

worker.onmessage = function (e) {
  // ここを追加
  console.log("結果を受信");
  result.textContent = e.data;
  // ここを追加
  console.log("scriptでのe.data: ", e.data);
};
onmessage = (e) => {
  console.log(e.data);
  // ここを追加
  console.log("依頼を受信");
  let total = e.data;
  for (let i = 1; i < 2000000000; i++) {
    total += 1;
  }
  // ここを追加
  console.log("workerでのtotal: ", total);
  // ここを追加
  console.log("結果を送信");
  postMessage(total);
};

コンソールログで見ると何がどの順番で実行されるかが理解しやすいですね。

ちなみにWorkerには注意点として「WorkerのJSファイル内ではDOM操作ができない」というものがあります。

先ほどのworker.js内でDOM操作をしてみます。

onmessage = (e) => {
  // ここを追加
 document.querySelector("#btn");
  console.log(e.data);
  console.log("依頼を受信");
  let total = e.data;
  for (let i = 1; i < 2000000000; i++) {
    total += 1;
  }
  console.log("workerでのtotal: ", total);
  console.log("結果を送信");
  postMessage(total);
};

Workerを担当するJSファイルではHTMLタグの取得や変更など、DOM操作になる処理は使用できないので注意しましょう。

  • このエントリーをはてなブックマークに追加