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 & 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 Worker
はService 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
はデータが複製されます。巨大データは Transferable(ArrayBuffer
等)で送るか、**共有メモリ(SharedArrayBuffer)**とAtomics
の利用を検討できます。 -
寿命と初期化の再実行
Shared Worker
は、最後のクライアントが去ると終了し得ます。初回接続時の初期化と、再起動時の初期化を正しく設計し、副作用やバグが出ないようにします。
まとめ
Shared Worker
は、複数ページで共有する長寿命の任意ロジックを置く場所です。- メッセージ契約(型/プロトコル)を明確にし、相関 ID を付けたリクエスト/レスポンス設計で堅牢にします。
- WebSocket の多重化や IndexedDB の直列化など、「一本化して全タブに効果が出る処理」に最適です。
- バンドラ/型定義/再接続などの細部を詰めると、保守性と体験が大きく向上します。
YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。