JavaScriptにおける`Shared Worker`

JavaScriptにおける`Shared Worker`

この記事ではJavaScriptにおけるShared Workerについて説明します。

Shared Workerの基本から実践的な利用例までをステップバイステップで解説します。

YouTube Video

javascript-shared-worker.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <script>
111        // Override console.log to display messages in the #output element
112        (function () {
113            // Override console.log
114            const originalLog = console.log;
115            console.log = function (...args) {
116                originalLog.apply(console, args);
117                const message = document.createElement('div');
118                message.textContent = args.map(String).join(' ');
119                output.appendChild(message);
120            };
121
122            // Override console.error
123            const originalError = console.error;
124            console.error = function (...args) {
125                originalError.apply(console, args);
126                const message = document.createElement('div');
127                message.textContent = args.map(String).join(' ');
128                message.style.color = 'red'; // Color error messages red
129                output.appendChild(message);
130            };
131        })();
132
133        document.getElementById('executeBtn').addEventListener('click', () => {
134            // Prevent multiple loads
135            if (document.getElementById('externalScript')) return;
136
137            const script = document.createElement('script');
138            script.src = 'javascript-shared-worker.js';
139            script.id = 'externalScript';
140            //script.onload = () => console.log('javascript-shared-worker.js loaded and executed.');
141            //script.onerror = () => console.log('Failed to load javascript-shared-worker.js.');
142            document.body.appendChild(script);
143        });
144    </script>
145</body>
146</html>

JavaScriptにおけるShared Worker

Shared Worker とは何か

Shared Worker は、同一オリジン内の複数ページ(タブ、ウィンドウ、iframe など)で共有されるワーカースレッドです。ページ専有のDedicated Workerと違い、1つのバックグラウンド処理を複数のページで共同利用できるのが最大の特徴です。典型的な用途は次のとおりです。

  • 複数タブで 単一の WebSocket 接続を共有し、接続数の削減や再接続の一本化を行えます。
  • タブ間の **状態同期(Pub/Subやストアの集中管理)**を行えます。
  • IndexedDB 操作の直列化を行い、同時アクセスの調停ができます。
  • 計算コストの高い処理の 重複実行を防止 できます。

Shared WorkerService Worker とも役割が異なります。Service Worker はネットワークプロキシが主眼で、Shared Workerページ間で共有する任意の計算や状態管理が主眼です。

基本 API とライフサイクル

作成(メインスレッド)

 1// main.js
 2// The second argument 'name' serves as an identifier to share the same SharedWorker
 3// if both the URL and name are the same.
 4const worker = new SharedWorker('/shared-worker.js', { name: 'app-core' });
 5
 6// Get the communication port (MessagePort) with this SharedWorker
 7const port = worker.port;
 8
 9// Receive messages with onmessage, send messages with postMessage
10port.onmessage = (ev) => {
11    console.log('From SharedWorker:', JSON.stringify(ev.data));
12};
13
14// When using onmessage, start() is not required
15// (start() is needed when using addEventListener instead)
16port.postMessage({ type: 'hello', from: location.pathname });
  • このコードは、Shared Worker を生成し、そのポートを介してメッセージの送受信を行う処理を示しています。

受け側(Shared Worker スコープ)

 1// shared-worker.js
 2// SharedWorkerGlobalScope has an onconnect event,
 3// and a MessagePort is provided for each connection
 4const ports = new Set();
 5
 6onconnect = (e) => {
 7    const port = e.ports[0];
 8    ports.add(port);
 9
10    port.onmessage = (ev) => {
11        // Log the received message and send a reply back to the sender
12        console.log('From page:', ev.data);
13        port.postMessage({ type: 'ack', received: ev.data });
14    };
15
16    // For handling explicit disconnection from the page (via MessagePort.close())
17    port.onmessageerror = (err) => {
18        console.error('Message error:', err);
19    };
20
21    // Greeting immediately after connection
22    port.postMessage({ type: 'welcome', clientCount: ports.size });
23};
24
25// Explicit termination of the SharedWorker is usually unnecessary
26// (It will be garbage collected when all ports are closed)
  • このコードは、Shared Worker 内で接続ごとに MessagePort を受け取り、クライアントからのメッセージを処理して返信する仕組みを実装しています。

Shared Workerのライフサイクル

Shared Workerは、最初の接続が張られた時に起動し、最後のポートが閉じられると終了し得ます。ページのリロード/クローズで接続は切れ、必要になれば再作成されます。

Shared Worker は「ブラウザ内で共有される長寿命スクリプト」です。Shared Worker のライフサイクルを意識しないと、リソースリークや古い状態の残存、意図しない再起動などが発生しやすくなります。

まず動かす:タブ間ブロードキャスト

複数タブで同じ Shared Worker に接続し、誰かが送ったメッセージを全タブに配信する最小実装です。

main.js

 1const worker = new SharedWorker('/shared-worker.js', { name: 'bus' });
 2const port = worker.port;
 3
 4port.onmessage = (ev) => {
 5    const msg = ev.data;
 6    if (msg.type === 'message') {
 7        const li = document.createElement('li');
 8        li.textContent = `[${new Date(msg.at).toLocaleTimeString()}] ${msg.payload}`;
 9        document.querySelector('#messages').appendChild(li);
10    } else if (msg.type === 'ready') {
11        console.log(`Connected. clients=${msg.clients}`);
12    }
13};
14
15document.querySelector('#form').addEventListener('submit', (e) => {
16    e.preventDefault();
17    const input = document.querySelector('#text');
18    port.postMessage({ type: 'publish', payload: input.value });
19    input.value = '';
20});
21
22document.querySelector('#ping').addEventListener('click', () => {
23    port.postMessage({ type: 'ping' });
24});
  • このコードは、Shared Worker と接続してメッセージの受信・表示や送信フォームからの投稿、ボタンクリックによる ping 送信を行う処理を実装しています。

shared-worker.js

 1// Minimal pub/sub bus in a SharedWorker
 2const subscribers = new Set();
 3
 4/** Broadcast to all connected ports */
 5function broadcast(message) {
 6    for (const port of subscribers) {
 7        try {
 8            port.postMessage(message);
 9        } catch (e) {
10            // Unsubscribe if sending fails, just in case
11            subscribers.delete(port);
12        }
13    }
14}
15
16onconnect = (e) => {
17    const port = e.ports[0];
18    subscribers.add(port);
19
20    port.onmessage = (ev) => {
21        const msg = ev.data;
22        if (msg && msg.type === 'publish') {
23            broadcast({ type: 'message', payload: msg.payload, at: Date.now() });
24        } else if (msg && msg.type === 'ping') {
25            port.postMessage({ type: 'pong', at: Date.now() });
26        }
27    };
28
29    port.postMessage({ type: 'ready', clients: subscribers.size });
30};
  • このコードは、Shared Worker 内で複数クライアント間のメッセージを中継する簡易的な pub/sub 機能を実装しています。

index.html

 1<!doctype html>
 2<html>
 3    <body>
 4        <form id="form">
 5            <input id="text" placeholder="say something" />
 6            <button>Send</button>
 7        </form>
 8        <button id="ping">Ping</button>
 9        <ul id="messages"></ul>
10        <script src="/main.js" type="module"></script>
11    </body>
12</html>
  • このページを複数タブで開くと、どのタブからメッセージを送っても他のタブにも通知されます。

メッセージ設計:リクエスト/レスポンスと相関 ID

Shared Worker と複数クライアントがやりとりすると、どの返信がどの依頼に対応するのかを識別したくなります。そのため、返信と依頼を対応付ける相関 ID(correlation id)を入れるのが定石です。

main.js

 1function createClient(workerUrl, name) {
 2    const w = new SharedWorker(workerUrl, { name });
 3    const port = w.port;
 4    const pending = new Map(); // id -> {resolve,reject}
 5    let nextId = 1;
 6
 7    port.onmessage = (ev) => {
 8        const { id, ok, result, error } = ev.data || {};
 9        if (!id || !pending.has(id)) return;
10        const { resolve, reject } = pending.get(id);
11        pending.delete(id);
12        ok ? resolve(result) : reject(new Error(error));
13    };
14
15    function call(method, params) {
16        const id = nextId++;
17        return new Promise((resolve, reject) => {
18            pending.set(id, { resolve, reject });
19            port.postMessage({ id, method, params });
20        });
21    }
22
23    return { call };
24}
25
26// usage
27const client = createClient('/shared-worker.js', 'rpc');
28client.call('add', { a: 2, b: 3 }).then(console.log);   // -> 5
29client.call('sleep', { ms: 500 }).then(console.log);     // -> 'done'
  • このコードは、Shared Worker と非同期でメソッド呼び出しをやり取りできる簡易 RPC クライアントを実装しています。 ここで、RPC サーバー(Remote Procedure Call サーバー)とは、別のプログラムやプロセスから関数や手続きを呼び出す仕組みを提供するサーバー のことです。
  • ここでは、idを単純にインクリメントしていますが、UUIDやランダム文字列を使う方法、またはタイムスタンプとカウンタを組み合わせて一意なキーを生成する方法も考えられます。

shared-worker.js

 1// A small request/response router with correlation ids
 2onconnect = (e) => {
 3    const port = e.ports[0];
 4
 5    port.onmessage = async (ev) => {
 6        const { id, method, params } = ev.data || {};
 7        try {
 8            let result;
 9            switch (method) {
10                case 'add':
11                    result = (params?.a ?? 0) + (params?.b ?? 0);
12                    break;
13                case 'sleep':
14                    await new Promise(r => setTimeout(r, params?.ms ?? 0));
15                    result = 'done';
16                    break;
17                default:
18                    throw new Error(`Unknown method: ${method}`);
19            }
20            port.postMessage({ id, ok: true, result });
21        } catch (err) {
22            port.postMessage({ id, ok: false, error: String(err) });
23        }
24    };
25};
  • このコードは、method に応じて処理を実行し、結果をリクエスト ID とともに返信する簡易 RPC サーバーを実装しています。

代表的な実戦パターン

Shared Workerを利用する上での実践的なパターンとして次のようなものがあります。

単一 WebSocket の多重化(タブ共有)

各タブが個別に WebSocket を開くとサーバ負荷と接続上限に影響します。Shared Workerひとつだけ WebSocket を置き、タブはワーカー経由で送受信します。

main.js

 1const w = new SharedWorker('/shared-worker.js', { name: 'ws' });
 2const port = w.port;
 3
 4port.onmessage = (ev) => {
 5    const msg = ev.data;
 6    if (msg.type === 'data') {
 7        console.log('Server says:', msg.payload);
 8    } else if (msg.type === 'socket') {
 9        console.log('WS state:', msg.state);
10    }
11};
12
13function send(payload) {
14    port.postMessage({ type: 'send', payload });
15}
  • このコードは、Shared Worker を介して WebSocket サーバーとのメッセージ送受信や接続状態の通知を受け取る処理を実装しています。

shared-worker.js

 1let ws;
 2const ports = new Set();
 3
 4function ensureSocket() {
 5    if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
 6        return;
 7    }
 8    ws = new WebSocket('wss://example.com/stream');
 9    ws.onopen = () => broadcast({ type: 'socket', state: 'open' });
10    ws.onclose = () => broadcast({ type: 'socket', state: 'closed' });
11    ws.onerror = (e) => broadcast({ type: 'socket', state: 'error', detail: String(e) });
12    ws.onmessage = (m) => broadcast({ type: 'data', payload: m.data });
13}
14
15function broadcast(msg) {
16    for (const p of ports) p.postMessage(msg);
17}
18
19onconnect = (e) => {
20    const port = e.ports[0];
21    ports.add(port);
22    ensureSocket();
23
24    port.onmessage = (ev) => {
25        const msg = ev.data;
26        if (msg?.type === 'send' && ws?.readyState === WebSocket.OPEN) {
27            ws.send(JSON.stringify(msg.payload));
28        }
29    };
30};
  • このコードは、Shared Worker 内で単一の WebSocket 接続を共有し、接続状態や受信データを全クライアントにブロードキャストしつつ、各クライアントからの送信要求を WebSocket 経由でサーバーに送信する仕組みを実装しています。

  • 再接続の戦略や、未送信キューのバッファリングを Shared Worker に集約すると、全タブで挙動が一貫します。

IndexedDB の調停(直列化)

複数タブから同じ DB にアクセスする際、同時トランザクションの競合やロック待ちを避けたい場合、Shared Workerでキューイングし、直列に処理できます。

 1// db-worker.js (very simplified – error handling omitted)
 2let db;
 3async function openDB() {
 4    if (db) return db;
 5    db = await new Promise((resolve, reject) => {
 6        const req = indexedDB.open('appdb', 1);
 7        req.onupgradeneeded = () => req.result.createObjectStore('kv');
 8        req.onsuccess = () => resolve(req.result);
 9        req.onerror = () => reject(req.error);
10    });
11    return db;
12}
13
14async function put(key, value) {
15    const d = await openDB();
16    return new Promise((resolve, reject) => {
17        const tx = d.transaction('kv', 'readwrite');
18        tx.objectStore('kv').put(value, key);
19        tx.oncomplete = () => resolve(true);
20        tx.onerror = () => reject(tx.error);
21    });
22}
23
24async function get(key) {
25    const d = await openDB();
26    return new Promise((resolve, reject) => {
27        const tx = d.transaction('kv', 'readonly');
28        const req = tx.objectStore('kv').get(key);
29        req.onsuccess = () => resolve(req.result);
30        req.onerror = () => reject(req.error);
31    });
32}
33
34onconnect = (e) => {
35    const port = e.ports[0];
36    port.onmessage = async (ev) => {
37        const { id, op, key, value } = ev.data;
38        try {
39            const result = op === 'put' ? await put(key, value) : await get(key);
40            port.postMessage({ id, ok: true, result });
41        } catch (err) {
42            port.postMessage({ id, ok: false, error: String(err) });
43        }
44    };
45};
  • このコードは、Shared Worker を介して IndexedDB への読み書きをキュー化し、複数タブからのアクセスを直列化して競合やロック待ちを回避する仕組みを実装しています。

モジュール化とバンドラのポイント

Shared Worker スクリプトを ES Module として書きたい場合、環境によって挙動やサポート状況が異なることがあります。実務では以下のどちらかを選ぶのが無難です。

  • クラシックワーカー形式で書き、必要なら importScripts() で依存を読み込みます。
  • バンドラ(Vite / Webpack / esbuild など)の ワーカーエントリ機能を使い、ビルド時に Shared Worker 向けバンドルを分けます。

エラー処理・切断検知・堅牢化のコツ

エラー処理や堅牢化のために次のようなポイントを考慮できます。

  • 未接続時送信の扱い ポートが準備できる前のメッセージは キューに積みます。

  • 切断検知 MessagePort に標準の onclose はありません。メイン側で beforeunload 時に port.postMessageで、{type:'bye'}のメッセージを送るなど、プロトコルで明示して、ワーカーのクリーンアップが行われるようにします。

  • 再接続 ページ再読み込みやタブ復帰で新しいポートができます。初期同期メッセージ(状態一括送信)を用意しておきます。

  • バックプレッシャ 大量ブロードキャスト時は、頻度制御(throttle/debounce)スナップショット送信に切り替えます。

  • セキュリティ Shared Worker は基本的に 同一オリジンで共有されます。ワーカー内に機密を置く場合は、トークン等を用いた呼び出し側の検証を設計に含めることができます。

Dedicated Worker / Shared Worker / Service Worker の使い分け

各種類のWorkerには、次のような特徴があります。

  • Dedicated Worker Dedicated Workerは、単一ページ専用です。UI と 1:1 の計算分離ができます。

  • Shared Worker Shared Workerは、同一オリジンの複数ページで共有されます。タブ間連携単一接続の共有に最適です。

  • Service Worker Service Workerは、ネットワーク仲介、キャッシュ、オフライン、Push、バックグラウンドでの同期処理などに利用できます。Fetch を横取りできるのが強みです。

「タブ間の情報共有と任意処理」なら Shared Worker、「ネットワーク制御」なら Service Worker、「重い処理を UI から外したいだけ」なら Dedicated Worker という目安が使えます。

よくある落とし穴

Shared Workerを利用する際には、次の点に注意する必要があります。

  • start() の呼び忘れや不要な呼び出し port.addEventListener('message', ...) では port.start() が必要です。port.onmessage = ... を使う場合は不要です。

  • 無制限ブロードキャスト タブが増えると負荷が増加します。差分配信購読トピック(topic でフィルタ)を導入することで負荷を低減できます。

  • オブジェクトのコピーコスト postMessage はデータが複製されます。巨大データは TransferableArrayBuffer 等)で送るか、**共有メモリ(SharedArrayBuffer)**と Atomics の利用を検討できます。

  • 寿命と初期化の再実行 Shared Workerは、最後のクライアントが去ると終了し得ます。初回接続時の初期化と、再起動時の初期化を正しく設計し、副作用やバグが出ないようにします。

まとめ

  • Shared Worker は、複数ページで共有する長寿命の任意ロジックを置く場所です。
  • メッセージ契約(型/プロトコル)を明確にし、相関 ID を付けたリクエスト/レスポンス設計で堅牢にします。
  • WebSocket の多重化IndexedDB の直列化など、「一本化して全タブに効果が出る処理」に最適です。
  • バンドラ/型定義/再接続などの細部を詰めると、保守性と体験が大きく向上します。

YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。

YouTube Video