Web Worker を使う

想定

このような TypeScript な NextJS 環境を想定してます。

依存のインストール

以下の8つをインストールします。

  • next @types/next
  • react @types/react
  • react-dom @types/react-dom
  • @types/node
  • typescript
yarn add {,@types/}{next,react,react-dom} @types/node typescript

トップページ作成

表示が確認できればいいので最低限でsrc/pages/index.tsxファイルを作ります。

mkdir -p src/pages && echo 'export default () => <div>🎉</div>' > src/pages/index.tsx

確認

以下でlocalhost:5489を開き「🎉」と出れば準備完了です。

npx npe scripts.dev 'next --port 5489'
yarn dev

WebWorker 導入

worker-loaderをインストールします。

yarn add -D worker-loader

worker.d.ts を作る

以下の内容で適当な位置(プロジェクト/など)にこのファイルを作ります。

declare module 'worker-loader?name=static/[hash].worker.js!*' {
  class WebpackWorker extends Worker {
    constructor();
  }

  export default WebpackWorker;
}

有効な位置に置けばworker-loader?name=static/[hash].worker.js!../workers/worker.tsとして読み込んだ時に上記の型で読み込めます。

nameオプションでstatic/以下に生成を指定するのは重要で、worker-loader が勝手に設定するパスだと.worker.jsが404で読み込めません。

👍 /_next/static/8f440f04e8526aebb804.worker.js
👎 /_next/8f440f04e8526aebb804.worker.js

Worker ファイルを作る

src/workers/ping-pong.worker.tsを作ります。何かメッセージが来たらpongと返すだけの Worker です。

NextJS により自動生成(または編集)されるtsconfig.jsonによってisolatedModulesが有効になります。このオプションによりモジュールになっていない.tsファイルは怒られます。それを回避する為に、一行目で適当にexportしてます。
このexportはのちにworker-loaderにより上書きされるで適当で大丈夫です。

//! To avoid isolatedModules error 
export default {};

self.addEventListener('message', () => {
  self.postMessage('pong');
});

まだpostMessageなどで型エラーが置きます。これはまだその部分がwindow.postMessageの型だと思ってるからです。

tsconfig.json

Web Worker を使うのでその型を取り込みます。tsconfig.jsoncompilerOptions.libwebworkerを加えます。

{
  "compilerOptions": {
    "lib": [
      "dom",
      "dom.iterable",
      "esnext",
      "webworker"
    ]
  }
}

これで先程のpostMessage周りの型エラーは消えたはずです。

next.config.js

コード分割機能でwindow["webpackChunk"]などにアクセスした時に Web Worker 上はwindowが無い為window is not definedエラーにならないように変更します。それには Webpack の設定を編集し、global.globalObjectselfを指定します。

module.exports = config => {
  config.output.globalObject = 'self';
  
  return config;
}

使う

あとは使うだけです。src/pages/index.tsxを以下のように書き換えます。

import React, { useEffect } from 'react';

export default () => {
  useEffect(() => {
    (async () => {
      const { default: Worker } = await import('worker-loader?name=static/[hash].worker.js!../workers/ping-pong.worker');

      const worker = new Worker();
      worker.addEventListener('message', event => {
        console.log(event.data);
      })
      worker.postMessage('ping');
    })();
  }, [])

  return <div>🎉</div>
}

Web Worker はブラウザでしか実行できないので、トップレベルでimportするとself周りで(self is not definedのような)エラーになります。なのでuseEffectの中で動的に読み込みます。その時のパスはworker.d.tsに則って記述します。

うまくいくと中身の詰まったdefaultが取り出せます。この中でnew Worker(動的に生成されたjsファイルへのパス)と Worker のインスタンスを作ってくれる関数になってます。

あとは戻り値の Worker で普段のように使うだけです。ping-pong.workerに対して適当なメッセージを送ります。その後ブラウザでpongとコンソールに表示されればうまくできてます!

ここでやったことの結果をリポジトリに置いておきます。

試したこと

@zeit/next-workersも試しましたが効きませんでした。ただ中のindex.jsを見入る感じおかしく無さそうには思いました。