Tips

【JavaScript】重たい処理をWorkerなしで速度改善する方法

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

デジタルが生活に無くなてはならない昨今において、プログラミングでは重たい処理をどうにかして緩和するかがポイントになります。

ベテランエンジニアに限った話ではなく、駆け出しエンジニアでも所属先のリーダーや顧客から求められることでしょう。

JavaScriptの場合だと代表的なテクニックの一つにWeb Workerというものがあり、重たい処理だけを別のファイルに逃して実行することがあります。

一方で駆け出しエンジニアの方の中にはWeb Workerを習っていなかったり、あまり理解できていないケースがあると思います。

今回はあえてWeb Workerを使わずに重たい処理を何とかするテクニックの一つを紹介します。

勉強中の方でも理解できる内容ですのでぜひ一緒にやってみましょう。

また動画もあるので必要に応じて活用してください。

JavaScriptでメモ化を作ってみる

結論から言うと1回処理した結果を保存しておく手法で「メモ化」とも呼ばれたりします。

以下のようなHTML、JSファイルを想定してみます。

<!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">
      <button id="btn">計算する</button>
    </div>
  </body>
</html>
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;
  }
  console.count(total);
});

画面に「計算する」というボタンがあり、こちらをクリックすると計算して結果をコンソールに表示するものです。

計算の内容としては20億回分だけ加算するだけですが、実行すると計算量が多いためにコンソールに結果が表示されるまで1〜2秒くらいのタイムラグが発生します。

1回だけだと許容できるかもしれませんが何回もやらないといけないとすると全体で何十秒もかかることになるわけです。

こちらの処理について少しでも動作を軽くするために以下のような変更を加えてみます。

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

// ここから変更
let value = null;
btn.addEventListener("click", () => {
  if (value != null) {
    console.count(value);
  } else {
    let total = 0;
    for (let i = 1; i < 2000000000; i++) {
      total += 1;
    }
    value = total;
    console.count(total);
  }
});

ポイントとしては変数valueを用意して、1回目の計算結果をvalueに代入する所です。

valueの初期値はnullにしておき、addEventListenerの中で条件分岐を作って、

・valueがnullでなければ、valueをそのまま表示して終了する
・valueがnullであれば計算をして結果を表示して終了する

という動きにしました。

「valueがnullではない」という状態は、1回目の計算が行われてvalueには計算結果が入っているということになります。

同じ計算をするのであれば結果も同じになるわけで、結果が同じであれば変数として保存しておくと毎回計算しなくて良くなるわけです。

この状態で再度実行してみます。

1回目の動きは変わらずタイムラグがありますが、2回目以降からはクリックしてからリアルタイムに結果が表示されているのが分かります。

同じようなものをWeb Workerで作るとなるとコード量が増えたり複雑になるかと思います。

あくまで状況によって対応を変える前提ではありますが、プログラミングの世界は正解は1個ではないことが多いです。

何気ない処理でも自分の工夫によってパフォーマンスを向上させたり、経験値の不足をカバーできるはずですので参考になれば幸いです。

Worker.jsを使ったメモ化

続いてもう一つの方法としてWorker.jsという標準のAPIを使ってみます。

こちらはライブラリでもないのでインストールなどは不要ですので使ったことのない人はぜひ一度試してみてください。

また最近のトレンドでWebサイトやWebアプリをPWA化する際にはWorker.jsが必須になります。

まず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が入っている
};

③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操作になる処理は使用できないので注意しましょう。

また今回参考にした本はこちらになります。

良ければどうぞ。

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