javascriptで画面ロックする場合は、Workerを使ってみよう

javascriptで重い処理を書く際やforやwhileなどのループを記述する際に気をつけないといけないのがUIロックです。

とくに重い処理を行うと、jsの処理に力を使ってしまって画面が固まってUIなどの操作性が格段に落ちてしまい、ブラウザなどからは応答に時間がかかっておりますといったアラートが表示される原因になります。
画面ロックが発生してしまうと、ユーザーは何もできなくなってしまうのでブラウザを強制終了するしかありません。そうなってしまうと、せっかくサイトに来た訪問者が何もせずに離脱して行くことにつながってしまいます。場合によっては、そのような問題が発生するサイトには二度とこないかもしれません。

そうした不具合や不具合やサイトの離脱を防ぐためにもjavascriptを使って重い処理をときはWorkerを使って重い処理を別タスクとして実行することをオススメします。
画面上の処理と重い処理を分けることができ、その結果画面をロックすることなく表示させることが可能になります。

今回はその方法を詳しく説明しますので、よろしくおねがいします。

UIをロックしてしまうループ処理

まずは画面をロックしてしまうような記述です。
下記のようなHTMLがあったとしましょう。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #animation:before {
      content: "";
      width: 5em;
      height: 5em;
      display: block;
      border: 2px solid black;
      margin: 50px auto;
    }
    @keyframes rotation {
      0%{ transform: rotate(0);}
      100%{ transform: rotate(360deg); }
    }
    .run #animation:before {
      animation: 2s linear infinite rotation;
    }
    .run button {
      background-color: red;
    }
  </style>
</head>
<body>
  <div id="animation"></div>
  <button type="button" onClick="run()">Run</button>
  <h1 id="counter"></h1>
  <h2 id="time"></h2>
</body>
</html>

このHTMLはcounter部分に文字を出力できるようにしてあります。
そして、実行時間をtime部分に出力します。
あまり使うことはないですが、今回は重い処理を行った場合を再現するということでこのような形にしました。

それでは、ここにfor文を使って文字を追加していってみましょう。今回はfor文が重い処理を行うという部分になります。
jsonやxml、データの解析や要素でforやwhile、eachなどを使うことは非常に多く、その後に何らかの処理を行うという記述もjavascriptを使っていれば高頻度で使用します。

const run = () => {
  document.body.classList.add('run');
  const start = Date.now();
  const elem = document.querySelector("#counter");
  for(let i = 0; i < 100000; i++) {
   elem.textContent = i;
  }
  document.querySelector("#time").textContent = Date.now() - start;
  document.body.classList.remove('run');
};

上のようなfor文で記述したスクリプトを作ってみました。
iの値をcounterに反映していくだけのものです。
まずはiの最大値を100,000くらいで実行してみましょう。
10万件のデータをフロントで処理するなと思うかもしれませんが、あくまで重い処理を行うとどうなるかという実験です。
10万件でなくても、数千件のデータで入れ子のループ処理が記述されている場合などは画面ロックが発生する確率も上がっていきます。

実行したところ、999,999と表示され、time部分の処理は569ミリ秒ということになります。

ここで注目してほしいのが、counterの文字を変化させているにもかかわらず、その文字が反映されていないという点です。
ChromeのDevelopper tools(macの場合[⌘ + option + i], Windowsの場合[Ctrl + Shift + I]または、F12)で確認しても、変化しているようではありますが、画面上では反映されていません。

重い処理を行う場合をフロントで行うと、 UX(ユーザー体験)の低下につながってしまいます。
そこで使うのが、 javascriptのタスクを別スレッドで実行できるWorker処理ということになります。

Workerとは

Workerとはバックグランド、つまり、裏の方で処理を行ってその結果をフロントに返却するということができる仕組みになります。

フロントで処理を行わないので、画面ロックを発生させることなく重い処理を実行できます。
Workerを使用するには実行したいファイル名を指定して呼びします。

そして、対象のワーカーに値などをpostMessageで値を渡します。
postMessage部分は配列や連想配列のデータでも構いません。

そして、対象のWorker側でpostされたデータを受信するように設定します。
受信の際はフロントであれば worker.addEventListener('message', (e) => {}) を、Workerであれば self.addEventListener('message', (e) => {}) を使用します。
処理内容を返却する際は、先ほどと同じようにpostMessageを使います。

const worker = new Worker(this.fileName);
const run = () => {
  worker.postMessage("run");
};
worker.addEventListener('message', (e) => {
  console.log(e.data);
}, false);
self.addEventListener('message', (e) => {
  //処理内容

  //処理結果を送信
  self.postMessage(e.data);
}, false);

UIをロックしないで行えるループ

それでは、先ほどロックしてしまったjsをworkerにしてみましょう。

const worker = new Worker("worker.js");
const run = () => {
  document.body.classList.add('run');
  const start = Date.now();
  const elem = document.querySelector("#counter");
  worker.postMessage("run");
  worker.addEventListener('message', (e) => {
    if(e.data.mode === 'end') {
      document.querySelector("#time").textContent = Date.now() - start;
      document.body.classList.remove('run');
    } else {
      document.querySelector("#counter").textContent = e.data.value;
    }
    console.log();
  }, false);
};

続いて、Worker部分の処理になります。
worker部分ではフロントのworker_main.jsから実行されたタイミングで動作を開始して、ループの値をフロントに戻すような処理を行います。
ループが終了すると、mode: endと終わったことを通知するようにしました。

self.addEventListener('message', (e) => {
  //処理内容
  for(let i = 0; i < 100000; i++) {
   console.log(i);
   self.postMessage({value: i});
  }
  self.postMessage({mode: 'end'});
  //処理結果を送信
}, false);

それでは実行してみましょう。

実行すると、先ほどとまったく違うUIになっているかと思います。
実はRunを押したタイミングで、ボタンを赤くして、上の四角が回転するようにしてありました。
Workerなしの処理ではjavascriptの処理が詰まってしまい、その部分の処理が正しく表示されていなかったということになります。

Workerは並列で処理を行える

画面ロックを防ぐ他に、WebWorkerには便利な機能があります。通常、javascriptはシングルスレッドなので並列(マルチスレッド)で処理を行うことができません
が、WebWorkerを用いることで、複数の処理を同時に行うことが可能になります。

  • フロントに関係のないデータの処理
  • 重い処理

上記のような処理をフロントで行うと、画面ロックが発生する原因となるほか、修正なども大変になりますが、webWorkerとして別にjavascriptを用意して実行されるようにしておくことで、メンテナンス性とユーザービリティーが向上します。

並列処理と聞くとpromiseなどを考えるかと思いますが、promiseは非同期処理であり、並列で処理は行いません。

WebWorkerを使用する上で注意すべきなのは、documentなどのフロントにある要素にはアクセスできないので、document.writeやdocument.querySelectorなどを使用できません。どうしても使用する場合は、変数としてworkerに対してpostするようにしましょう。

利用できる関数やAPIなどが下記に一覧で記載されているので参考にしてください。
Web Workers が使用できる関数とクラス

setTimeoutでもできるけど、オススメしない

UIのロックを防ぐ方法として、setTimeoutを使う方法もあります。
この方法はworkerを使っていないのですが、setTimeoutを使用することで別タスクとして処理させることができます。

const run = () => {
  document.body.classList.add('run');
  const start = Date.now();
  const elem = document.querySelector("#counter");
  for(let i = 0; i < 100000; i++) {
    setTimeout(()=>{
      console.log(i);
      elem.textContent = i;
    }, 0);
  }
  document.querySelector("#time").textContent = Date.now() - start;
  document.body.classList.remove('run');
};

数字部分は変化しますが、さきほどのようにアニメーションは行われません。
forの処理が先に終了してしまうため、cssのアニメーションが一瞬で終了してしまうのです。

まとめ

javascriptを使っていると思い処理も当然行う場合があります。そうしたときに、フロントで処理をしてしまうとローディングなどのアニメーションが正しく行われなかったりする原因になってしまいます。
Workerを使って、処理を別タスクとしてやることで正しくローディングなどを表示させることが可能です。

重い処理を行う際は、Workerを使って別タスクで行ってユーザーにストレスを与えないサイトを作っていきましょう。

オススメの書籍

第2回 pythonでNQueen(エイトクイーン)ブルートフォース 力任せ探索(2)

第2回 pythonでNQueen(エイトクイーン)ブルートフォース 力任せ探索(2)

パソコンは不要。スマホ、タブレットでできるブログ投稿

パソコンは不要。スマホ、タブレットでできるブログ投稿