2018年11月15日

Create React AppでWeb Workerを使うには

「Web Worker」を使うと、ブラウザ上のJavascriptでも複数のスレッドを使うことが出来るようになります。


Web Worker を使用する - Web API | MDN https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Using_web_workers#Web_Workers_API


これによって、CPU負荷の高い計算処理などを別スレッドで実行することで、画面の反応が鈍くなったりするのを防ぐことが出来ます。


今回は、Create React Appで作成したプロジェクトでWeb Workerを使う方法を調査しました。


1. publicフォルダにWorkerのJSファイルを配置して読み込む。


⇒ 最も基本的で単純な方法です。publicフォルダに配置したファイルはそのままデプロイされるので下の方法で素直に読み込んで使うことが出来ます。

const myWorker = new Worker("worker.js");
myWorker.postMessage("hoge");


短所としては、TypeScriptやES2017などを使っている場合は別途自分でビルドしてpublicフォルダに配置する必要があることが挙げられます。


2. ejectしてからWebPackの設定に worker-loader または worker-plugin を追加する。


⇒ worker-loaderを使うと、別スレッドで動かしたい処理をModuleとして作成しておいて、使いたい箇所でそれをimportしてnewするだけでWeb Workerとして動作するようになります。コード例は、下のようになります。

import HelloWorker from './hello.worker.js';
const helloWorker = new HelloWorker();
helloWorker.postMessage('hoge');

詳細はこちら。
Add support for WebWorker with worker-loader (#3660) by iansu · Pull Request #3934 · facebook/create-react-app 

worker-plugin を使う場合はこんな感じ。

const worker = new Worker('./worker', { type: 'module' });
worker.postMessage('hoge');

詳細はこちら。
Add support for WebWorker with worker-plugin by Krivega · Pull Request #5464 · facebook/create-react-app  




3. ejectせずに react-app-rewired を使ってWebPackの設定に worker-loader または worker-plugin を追加する。


⇒ 下のブログ記事にejectせずに上の2と同じ事をする方法が書かれていました。

How to use web workers with react-create-app and not ejecting in the attempt

ただ残念ながら、react-app-rewiredは最近のVersionには非対応なのでこの方法は現在は使えないようです。


4. WorkerのJSファイルをBlobとして読み込んでからWorkerスレッドを生成する。


⇒ 下のissueへのコメントで紹介されている方法です。

Is it possible to use load webworkers? · Issue #1277 · facebook/create-react-app

関数をtoString()で文字列化して、さらにそれをBlobにします。こうすると、WorkerのコンストラクタにObjectURLとして渡してWorkerインスタンスを生成出来るようです。

let code = worker.toString();
code = code.substring(code.indexOf("{")+1, code.lastIndexOf("}"));
const blob = new Blob([code], {type: "application/javascript"});
const worker = new Worker(URL.createObjectURL(blob));

少々トリッキーですが、試してみたところ少なくともMacのChrome/Safari/FireFoxでは問題なく動いています。

この方法なら、WebPackの設定を変更する必要がありません。また上記1のようにWorkerのJSファイルだけ自前でビルド・配置したりする必要もありません。

具体的なコード例は後述します。


5. 公式にサポートされるまで待つ。


⇒ 上の2で挙げた方法がPull Requestとして上がっており公式にも検討されているみたいですが、どうやらブラウザ互換性の問題(?)があるようで2018年11月時点ではまだマージされるには至っていません。将来的には公式にサポートされる可能性はあるので、気には止めておいた方が良いかも知れません。

Add support for WebWorker with worker-loader (#3660) by iansu · Pull Request #3934 · facebook/create-react-app 

Add support for WebWorker with worker-plugin by Krivega · Pull Request #5464 · facebook/create-react-app 



今回は上記4の方法を試してみたので、以下に紹介しておきます。

WorkerのJSファイルをBlobとして読み込んでからWorkerスレッドを生成するサンプル


index.jsと同じフォルダにcreateWorker.js, myWorker.jsの2つのファイルを作成します。

createWorker.js
export default (workerFunc: () => void) => {
let code = workerFunc.toString();
code = code.substring(code.indexOf('{') + 1, code.lastIndexOf('}'));
const blob = new Blob([code], { type: 'application/javascript' });
return new Worker(URL.createObjectURL(blob));
};
view raw createWorker.js hosted with ❤ by GitHub

---

myWorker.js
/* eslint no-restricted-globals: ["error"] */
export default () => {
let count = 0;
self.addEventListener(
'message',
function(e) {
if (e.data.action === 'count') {
count++;
}
if (e.data.action === 'clear') {
count = 0;
}
postMessage({ msg: 'count' + count, count: count });
},
false
);
};
view raw myWorker.js hosted with ❤ by GitHub

---


createWorkerとmyWorkerをimportして、下のようにすることでWorkerスレッドが使用可能になります。

import createWorker from './createWorker';
import myWorkerFunc from './myWorker';
const myWorker = createWorker(myWorkerFunc);


index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import createWorker from './createWorker';
import myWorkerFunc from './myWorker';
ReactDOM.render(<App />, document.getElementById('root'));
// Testing Web Worder
const myWorker = createWorker(myWorkerFunc);
myWorker.addEventListener('message', (m: any) => {
console.log('msg', m.data.msg);
if (m.data.count >= 10) {
console.log('clearing');
myWorker.postMessage({ action: 'clear' });
}
});
setInterval(() => {
myWorker.postMessage({ action: 'count' });
}, 400);
view raw index.js hosted with ❤ by GitHub

---