Shared Worker di JavaScript

Shared Worker di JavaScript

Artikel ini menjelaskan tentang Shared Worker di JavaScript.

Kami akan menjelaskan semuanya mulai dari dasar-dasar Shared Worker hingga contoh penggunaannya secara praktis, langkah demi langkah.

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>

Shared Worker di JavaScript

Apa itu Shared Worker?

Shared Worker adalah thread worker yang dapat digunakan bersama oleh beberapa halaman (tab, jendela, iframe, dan lain-lain) dalam origin yang sama. Tidak seperti Dedicated Worker yang khusus untuk halaman tertentu, fitur utamanya adalah proses latar belakang tunggal dapat dibagi di beberapa halaman. Contoh penggunaan yang umum antara lain:.

  • Anda dapat berbagi satu koneksi WebSocket di beberapa tab, mengurangi jumlah koneksi dan memusatkan proses koneksi ulang.
  • Anda dapat menyinkronkan status antar tab (manajemen terpusat untuk Pub/Sub atau store).
  • Anda dapat menyusun operasi IndexedDB secara berurutan, untuk memediasi akses secara bersamaan.
  • Anda dapat mencegah pelaksanaan proses yang memerlukan komputasi berat secara duplikat.

Shared Worker memiliki peran yang berbeda dari Service Worker. Service Worker terutama berfungsi sebagai proxy jaringan, sedangkan Shared Worker berfokus pada melakukan perhitungan arbitrer atau manajemen status yang dibagikan antar halaman.

API Dasar dan Siklus Hidup

Membuat (Thread Utama)

 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 });
  • Kode ini menunjukkan cara membuat Shared Worker dan mengirim/menerima pesan melalui port-nya.

Penerima (dalam lingkup 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)
  • Kode ini menunjukkan bagaimana, di dalam Shared Worker, sebuah MessagePort diterima untuk setiap koneksi dan memproses serta membalas pesan dari klien.

Siklus Hidup dari Shared Worker

Shared Worker dimulai ketika koneksi pertama terjalin dan dapat berhenti ketika port terakhir ditutup. Koneksi ditutup saat halaman dimuat ulang/ditutup, dan worker akan dibuat ulang jika diperlukan.

Shared Worker adalah "skrip berjangka panjang yang dibagikan di dalam peramban". Jika Anda tidak memperhatikan siklus hidup Shared Worker, masalah seperti kebocoran sumber daya, persistensi status yang usang, dan restart yang tidak diinginkan lebih mungkin terjadi.

Coba Sendiri: Broadcast Antar Tab

Ini adalah implementasi minimal di mana beberapa tab terhubung ke Shared Worker yang sama dan setiap pesan yang dikirim akan disiarkan ke semua tab.

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});
  • Kode ini menghubungkan ke Shared Worker, mengimplementasikan penerimaan/tampilan pesan, mengirim pesan dari formulir kirim, dan mengirim ping dengan menekan sebuah tombol.

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};
  • Kode ini mengimplementasikan fitur pub/sub sederhana di dalam Shared Worker untuk mengirimkan pesan antar berbagai klien.

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>
  • Jika Anda membuka halaman ini di beberapa tab, pesan yang dikirim dari tab mana pun akan diberitahukan ke tab lainnya.

Desain Pesan: Permintaan/Tanggapan dan Correlation ID

Ketika beberapa klien berinteraksi dengan Shared Worker, seringkali Anda ingin mengidentifikasi respon mana yang sesuai dengan permintaan tertentu. Oleh karena itu, praktik standar adalah menyertakan ID korelasi untuk menghubungkan respons dengan permintaan.

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'
  • Kode ini mengimplementasikan klien RPC sederhana yang dapat memanggil metode secara asinkron ke Shared Worker. Di sini, server RPC (Remote Procedure Call) mengacu pada server yang menyediakan mekanisme untuk memanggil fungsi dan prosedur dari program atau proses lain.
  • Pada contoh ini, id hanya ditingkatkan secara sederhana, tetapi Anda juga dapat menggunakan UUID, string acak, atau menggabungkan timestamp dengan penghitung untuk membuat kunci unik.

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};
  • Kode ini mengimplementasikan server RPC sederhana yang melakukan pemrosesan sesuai dengan method dan mengirimkan hasilnya bersama dengan ID permintaan.

Pola Penggunaan Umum

Ada beberapa pola praktis saat menggunakan Shared Worker, seperti berikut:.

Multiplexing WebSocket Tunggal (Bersama Antar Tab)

Jika setiap tab membuka koneksi WebSocket sendiri, hal ini akan mempengaruhi beban server dan batas koneksi. Tempatkan hanya satu WebSocket di dalam Shared Worker, dan setiap tab mengirim/menerima pesan melalui worker tersebut.

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}
  • Kode ini mengimplementasikan proses pengiriman/penerimaan pesan ke server WebSocket dan menerima notifikasi status koneksi melalui Shared Worker.

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};
  • Kode ini mengimplementasikan mekanisme di dalam Shared Worker untuk membagi satu koneksi WebSocket, menyiarkan status koneksi dan data yang diterima ke semua klien, serta mengirim permintaan keluar dari masing-masing klien ke server melalui WebSocket.

  • Dengan memusatkan strategi koneksi ulang dan antrean pesan yang belum terkirim di Shared Worker, perilaku menjadi konsisten di semua tab.

Mediasi IndexedDB (Serialisasi)

Saat mengakses database yang sama dari beberapa tab, jika Anda ingin menghindari konflik dari transaksi bersamaan atau menunggu kunci, Anda dapat mengantrikannya di dalam Shared Worker dan memprosesnya secara serial.

 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};
  • Kode ini mengimplementasikan mekanisme di mana operasi baca/tulis ke IndexedDB diantrikan melalui Shared Worker, men-serialisasi akses dari beberapa tab untuk menghindari konflik dan persaingan kunci.

Tips Modularisasi dan Bundler

Jika Anda ingin menulis skrip Shared Worker Anda sebagai ES Module, perilaku dan dukungan dapat bervariasi tergantung lingkungan. Dalam praktik, lebih aman untuk memilih salah satu dari berikut:.

  • Tulis dalam format worker klasik, dan gunakan importScripts() untuk memuat dependensi jika diperlukan.
  • Gunakan fitur entry worker dari bundler Anda (seperti Vite / Webpack / esbuild, dll.), dan buat bundle terpisah untuk Shared Worker saat proses build.

Tips Penanganan Error, Deteksi Putus Koneksi, dan Ketahanan

Pertimbangkan poin-poin berikut terkait penanganan error dan ketahanan:.

  • Menangani Pengiriman Sebelum Koneksi Terjadi Antrikan pesan yang datang sebelum port siap.

  • Deteksi Putus Koneksi MessagePort tidak memiliki handler standar onclose. Di sisi utama, kirim pesan {type: 'bye'} menggunakan port.postMessage saat beforeunload, atau jelaskan secara jelas dalam protokol untuk memastikan worker dibersihkan.

  • Koneksi Ulang Saat halaman dimuat ulang atau tab dibuka kembali, port baru akan dibuat. Siapkan pesan sinkronisasi awal (untuk mengirimkan status lengkap secara sekaligus).

  • Tekanan Balik (Backpressure) Pada saat penyiaran berat, lakukan throttling/debouncing atau mengirimkan snapshot.

  • Keamanan Shared Worker secara fundamental dibagikan dalam origin yang sama. Jika Anda menempatkan data rahasia di dalam worker, pertimbangkan merancang verifikasi sisi pemanggil dengan token atau metode serupa.

Cara Menggunakan Dedicated Worker, Shared Worker, dan Service Worker dengan Tepat

Setiap jenis Worker memiliki karakteristik sebagai berikut:.

  • Dedicated Worker Dedicated Worker dimaksudkan hanya untuk digunakan oleh satu halaman saja. Ini memungkinkan pemisahan komputasi secara 1:1 dari antarmuka pengguna (UI).

  • Shared Worker Shared Worker dapat digunakan bersama oleh beberapa halaman dengan asal (origin) yang sama. Ini sangat ideal untuk komunikasi antar tab dan berbagi satu koneksi.

  • Service Worker Service Worker dapat digunakan untuk proxy jaringan, caching, operasi offline, notifikasi push, dan sinkronisasi di latar belakang. Kekuatan utamanya adalah kemampuannya untuk mencegat permintaan fetch.

Sebagai aturan umum: gunakan Shared Worker untuk 'berbagi informasi dan pemrosesan arbitrer antar tab', Service Worker untuk 'kontrol jaringan', dan Dedicated Worker jika Anda hanya ingin memindahkan pemrosesan berat dari UI.

Kesalahan Umum

Saat menggunakan Shared Worker, Anda perlu memperhatikan hal-hal berikut.

  • Lupa memanggil start() atau pemanggilan yang tidak diperlukan Saat menggunakan port.addEventListener('message', ...), Anda harus memanggil port.start(). Ini tidak diperlukan jika Anda menggunakan port.onmessage = ....

  • Penyiaran Tanpa Batas Beban akan meningkat seiring bertambahnya jumlah tab. Anda dapat mengurangi beban dengan menerapkan differential delivery atau topik langganan (penyaringan berdasarkan topik).

  • Biaya Penyalinan Objek postMessage menduplikasi data. Untuk data besar, pertimbangkan untuk mengirimnya sebagai Transferable (seperti ArrayBuffer) atau menggunakan memori bersama (SharedArrayBuffer) bersama Atomics.

  • Masa Aktif dan Inisialisasi Ulang Shared Worker dapat berhenti ketika klien terakhir terputus. Rancang inisialisasi dengan benar untuk koneksi pertama dan saat restart guna menghindari efek samping dan bug.

Ringkasan

  • Shared Worker adalah tempat untuk mengimplementasikan logika kustom jangka panjang yang dibagikan di beberapa halaman.
  • Perjelas kontrak pesan (tipe/protokol) dan buat desain permintaan/respons Anda tangguh dengan melampirkan correlation ID.
  • Ini ideal untuk proses yang bekerja lebih baik ketika disatukan di semua tab, seperti multiplexing koneksi WebSocket atau serialisasi akses ke IndexedDB.
  • Penyempurnaan detail seperti bundler, definisi tipe, dan mekanisme reconnect dapat sangat meningkatkan kemudahan pemeliharaan dan pengalaman pengguna.

Anda dapat mengikuti artikel di atas menggunakan Visual Studio Code di saluran YouTube kami. Silakan periksa juga saluran YouTube kami.

YouTube Video