Dasar-Dasar Pekerja Web

Masalah: Konkurensi JavaScript

Ada sejumlah bottleneck yang mencegah aplikasi menarik ditransfer (misalnya, dari implementasi server yang berat) ke JavaScript sisi klien. Beberapa di antaranya mencakup kompatibilitas browser, pengetikan statis, aksesibilitas, dan performa. Untungnya, tidak ada yang berubah dengan cepat karena vendor browser dengan cepat meningkatkan kecepatan mesin JavaScript mereka.

Satu hal yang tetap menjadi hambatan bagi JavaScript sebenarnya adalah bahasa itu sendiri. JavaScript adalah lingkungan thread tunggal, yang berarti beberapa skrip tidak dapat dijalankan secara bersamaan. Misalnya, bayangkan sebuah situs yang perlu menangani peristiwa UI, membuat kueri, dan memproses data API dalam jumlah besar, serta memanipulasi DOM. Cukup umum, bukan? Sayangnya, semua itu tidak dapat dilakukan secara bersamaan karena keterbatasan runtime JavaScript browser. Eksekusi skrip terjadi dalam satu thread.

Developer meniru 'konkurensi' dengan menggunakan teknik seperti setTimeout(), setInterval(), XMLHttpRequest, dan pengendali peristiwa. Ya, semua fitur ini berjalan secara asinkron, tetapi non-pemblokiran tidak berarti konkurensi. Peristiwa asinkron diproses setelah skrip eksekusi saat ini dihasilkan. Kabar baiknya adalah HTML5 memberi kita sesuatu yang lebih baik daripada peretasan ini!

Memperkenalkan Web Worker: menghadirkan threading ke JavaScript

Spesifikasi Pekerja Web menentukan API untuk menghasilkan skrip latar belakang di aplikasi web Anda. Dengan Pekerja Web, Anda dapat melakukan hal-hal seperti mengaktifkan skrip yang berjalan lama untuk menangani tugas yang menggunakan banyak komputasi, tetapi tanpa memblokir UI atau skrip lainnya untuk menangani interaksi pengguna. Mereka akan membantu mengakhiri dialog 'skrip tidak responsif' yang menjengkelkan yang kita semua sukai:

Dialog skrip tidak responsif
Dialog skrip yang tidak responsif umum.

Pekerja memanfaatkan penerusan pesan seperti rangkaian pesan untuk mencapai paralelisme. Fitur ini sangat cocok untuk menjaga refresh UI Anda, berperforma tinggi, dan responsif bagi pengguna.

Jenis Pekerja Web

Perlu diperhatikan bahwa spesifikasi membahas dua jenis Pekerja Web, Pekerja Khusus dan Pekerja Bersama. Artikel ini hanya akan membahas pekerja berdedikasi. Saya akan menyebutnya sebagai 'web worker' atau 'pekerja' secara keseluruhan.

Memulai

Web Worker berjalan di thread yang terisolasi. Akibatnya, kode yang mereka eksekusi perlu dimuat dalam file terpisah. Namun sebelum melakukannya, hal pertama yang harus dilakukan adalah membuat objek Worker baru di halaman utama Anda. Konstruktor mengambil nama skrip pekerja:

var worker = new Worker('task.js');

Jika file yang ditentukan ada, browser akan menghasilkan thread pekerja baru, yang didownload secara asinkron. Pekerja tidak akan dimulai hingga file telah didownload dan dieksekusi sepenuhnya. Jika jalur ke pekerja Anda menampilkan 404, pekerja akan gagal tanpa ada peringatan.

Setelah membuat pekerja, mulailah dengan memanggil metode postMessage():

worker.postMessage(); // Start the worker.

Berkomunikasi dengan pekerja melalui penerusan pesan

Komunikasi antara pekerjaan dan halaman induknya dilakukan menggunakan model peristiwa dan metode postMessage(). Bergantung pada browser/versi Anda, postMessage() dapat menerima string atau objek JSON sebagai argumen tunggalnya. Versi terbaru browser modern mendukung penerusan objek JSON.

Di bawah ini adalah contoh penggunaan string untuk meneruskan 'Hello World' ke pekerja di doWork.js. Worker hanya menampilkan pesan yang diteruskan kepadanya.

Skrip utama:

var worker = new Worker('doWork.js');

worker.addEventListener('message', function(e) {
console.log('Worker said: ', e.data);
}, false);

worker.postMessage('Hello World'); // Send data to our worker.

doWork.js (pekerja):

self.addEventListener('message', function(e) {
self.postMessage(e.data);
}, false);

Saat postMessage() dipanggil dari halaman utama, pekerja kami akan menangani pesan tersebut dengan menentukan pengendali onmessage untuk peristiwa message. Payload pesan (dalam hal ini 'Hello World') dapat diakses di Event.data. Meskipun contoh ini tidak terlalu menarik, contoh ini menunjukkan bahwa postMessage() juga merupakan sarana Anda untuk meneruskan data kembali ke thread utama. Praktis!

Pesan yang diteruskan antara halaman utama dan pekerja akan disalin, tidak dibagikan. Misalnya, dalam contoh berikutnya, properti 'msg' dari pesan JSON dapat diakses di kedua lokasi. Tampaknya objek diteruskan langsung ke pekerja meskipun berjalan di ruang khusus yang terpisah. Sebenarnya, yang terjadi adalah objek diserialisasi saat diserahkan ke pekerja, dan kemudian, dide-serialisasi di ujung lain. Halaman dan pekerja tidak memiliki instance yang sama, sehingga hasil akhirnya adalah duplikat dibuat di setiap kartu. Sebagian besar browser menerapkan fitur ini dengan encoding/decoding JSON secara otomatis di kedua ujung.

Berikut adalah contoh lebih kompleks yang meneruskan pesan menggunakan objek JSON.

Skrip utama:

<button onclick="sayHI()">Say HI</button>
<button onclick="unknownCmd()">Send unknown command</button>
<button onclick="stop()">Stop worker</button>
<output id="result"></output>

<script>
function sayHI() {
worker.postMessage({'cmd': 'start', 'msg': 'Hi'});
}

function stop() {
// worker.terminate() from this script would also stop the worker.
worker.postMessage({'cmd': 'stop', 'msg': 'Bye'});
}

function unknownCmd() {
worker.postMessage({'cmd': 'foobard', 'msg': '???'});
}

var worker = new Worker('doWork2.js');

worker.addEventListener('message', function(e) {
document.getElementById('result').textContent = e.data;
}, false);
</script>

doWork2.js:

self.addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'start':
    self.postMessage('WORKER STARTED: ' + data.msg);
    break;
case 'stop':
    self.postMessage('WORKER STOPPED: ' + data.msg +
                    '. (buttons will no longer work)');
    self.close(); // Terminates the worker.
    break;
default:
    self.postMessage('Unknown command: ' + data.msg);
};
}, false);

Objek yang dapat ditransfer

Sebagian besar browser menerapkan algoritma cloning terstruktur, yang memungkinkan Anda meneruskan jenis yang lebih kompleks ke dalam/ke luar Pekerja seperti objek File, Blob, ArrayBuffer, dan JSON. Namun, saat meneruskan jenis data ini menggunakan postMessage(), salinan akan tetap dibuat. Oleh karena itu, jika Anda meneruskan file besar 50 MB (misalnya), ada overhead yang signifikan saat memindahkan file tersebut antara pekerja dan thread utama.

Cloning terstruktur memang bagus, tetapi salinan dapat memerlukan waktu ratusan milidetik. Untuk mengatasi hit perf, Anda dapat menggunakan Objek yang Dapat Ditransfer.

Dengan Objek yang Dapat Ditransfer, data akan ditransfer dari satu konteks ke konteks lainnya. Ini adalah zero-copy, yang sangat meningkatkan performa pengiriman data ke Pekerja. Anggap saja ini sebagai referensi {i>pass-by-reference <i} jika Anda berasal dari dunia C/C++. Namun, tidak seperti referensi yang lewat, 'versi' dari konteks panggilan tidak lagi tersedia setelah ditransfer ke konteks baru. Misalnya, saat mentransfer ArrayBuffer dari aplikasi utama Anda ke Worker, ArrayBuffer asli akan dihapus dan tidak dapat digunakan lagi. Kontennya (secara harfiah) ditransfer ke konteks Pekerja.

Untuk menggunakan objek yang dapat ditransfer, gunakan tanda tangan postMessage() yang sedikit berbeda:

worker.postMessage(arrayBuffer, [arrayBuffer]);
window.postMessage(arrayBuffer, targetOrigin, [arrayBuffer]);

Kasus pekerja, argumen pertama adalah data, dan yang kedua adalah daftar item yang harus ditransfer. Argumen pertama omong-omong tidak harus berupa ArrayBuffer. Misalnya, item baris dapat berupa objek JSON:

worker.postMessage({data: int8View, moreData: anotherBuffer},
                [int8View.buffer, anotherBuffer]);

Poin pentingnya adalah: argumen kedua harus berupa array ArrayBuffer. Ini adalah daftar item yang dapat ditransfer.

Untuk informasi selengkapnya tentang transferrable, lihat postingan kami di developer.chrome.com.

Lingkungan pekerja

Cakupan pekerja

Dalam konteks pekerja, self dan this mereferensikan cakupan global untuk pekerja. Dengan demikian, contoh sebelumnya juga dapat ditulis sebagai:

addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'start':
    postMessage('WORKER STARTED: ' + data.msg);
    break;
case 'stop':
...
}, false);

Atau, Anda dapat menetapkan pengendali peristiwa onmessage secara langsung (meskipun addEventListener selalu disarankan oleh ninja JavaScript).

onmessage = function(e) {
var data = e.data;
...
};

Fitur yang tersedia untuk pekerja

Karena perilaku multi-thread, Web Worker hanya memiliki akses ke subset fitur JavaScript:

  • Objek navigator
  • Objek location (hanya baca)
  • XMLHttpRequest
  • setTimeout()/clearTimeout() dan setInterval()/clearInterval()
  • Cache Aplikasi
  • Mengimpor skrip eksternal menggunakan metode importScripts()
  • Melahirkan pekerja web lainnya

Pekerja TIDAK memiliki akses ke:

  • DOM (tidak aman untuk thread)
  • Objek window
  • Objek document
  • Objek parent

Memuat skrip eksternal

Anda dapat memuat library atau file skrip eksternal ke pekerja dengan fungsi importScripts(). Metode ini mengambil nol string atau lebih yang mewakili nama file untuk resource yang akan diimpor.

Contoh ini memuat script1.js dan script2.js ke pekerja:

worker.js:

importScripts('script1.js');
importScripts('script2.js');

Yang juga dapat ditulis sebagai pernyataan impor tunggal:

importScripts('script1.js', 'script2.js');

Sub-kerja

Pekerja memiliki kemampuan untuk menghasilkan pekerja anak. Cara ini bagus untuk memecah tugas-tugas besar lebih lanjut saat runtime. Namun, subpekerja memiliki beberapa catatan:

  • Subpekerja harus dihosting dalam asal yang sama dengan halaman induk.
  • URI dalam subpekerja diselesaikan sesuai dengan lokasi pekerja induknya (bukan halaman utama).

Perlu diingat bahwa sebagian besar browser memunculkan proses terpisah untuk setiap pekerja. Sebelum Anda melahirkan pekerja peternakan, berhati-hatilah agar tidak menghabiskan terlalu banyak resource sistem pengguna. Salah satu alasannya adalah pesan yang diteruskan antara halaman utama dan pekerja disalin, tidak dibagikan. Lihat Berkomunikasi dengan Pekerja melalui Penerusan Pesan.

Untuk melihat contoh cara memunculkan subpekerja, lihat contoh dalam spesifikasi.

Pekerja inline

Bagaimana jika Anda ingin membuat skrip pekerja dengan cepat, atau membuat halaman mandiri tanpa harus membuat file pekerja terpisah? Dengan Blob(), Anda dapat "menyejajarkan" pekerja dalam file HTML yang sama dengan logika utama dengan membuat handle URL ke kode pekerja sebagai string:

var blob = new Blob([
"onmessage = function(e) { postMessage('msg from worker'); }"]);

// Obtain a blob URL reference to our worker 'file'.
var blobURL = window.URL.createObjectURL(blob);

var worker = new Worker(blobURL);
worker.onmessage = function(e) {
// e.data == 'msg from worker'
};
worker.postMessage(); // Start the worker.

URL blob

Keajaiban hadir dengan panggilan ke window.URL.createObjectURL(). Metode ini membuat string URL sederhana yang dapat digunakan untuk mereferensikan data yang disimpan dalam objek File atau Blob DOM. Contoh:

blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1

URL blob bersifat unik dan berlaku selama masa aktif aplikasi (misalnya hingga document dibongkar muatannya). Jika Anda membuat banyak URL Blob, sebaiknya rilis referensi yang tidak lagi diperlukan. Anda dapat merilis URL Blob secara eksplisit dengan meneruskannya kewindow.URL.revokeObjectURL():

window.URL.revokeObjectURL(blobURL);

Di Chrome, ada halaman yang bagus untuk melihat semua URL blob yang dibuat: chrome://blob-internals/.

Contoh lengkap

Melangkah lebih jauh, kita dapat mempelajari cara kode JS pekerja disisipkan di halaman kita. Teknik ini menggunakan tag <script> untuk menentukan pekerja:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>

<div id="log"></div>

<script id="worker1" type="javascript/worker">
// This script won't be parsed by JS engines
// because its type is javascript/worker.
self.onmessage = function(e) {
    self.postMessage('msg from worker');
};
// Rest of your worker code goes here.
</script>

<script>
function log(msg) {
    // Use a fragment: browser will only render/reflow once.
    var fragment = document.createDocumentFragment();
    fragment.appendChild(document.createTextNode(msg));
    fragment.appendChild(document.createElement('br'));

    document.querySelector("#log").appendChild(fragment);
}

var blob = new Blob([document.querySelector('#worker1').textContent]);

var worker = new Worker(window.URL.createObjectURL(blob));
worker.onmessage = function(e) {
    log("Received: " + e.data);
}
worker.postMessage(); // Start the worker.
</script>
</body>
</html>

Menurut saya, pendekatan baru ini sedikit lebih rapi dan lebih mudah dibaca. Atribut ini menentukan tag skrip dengan id="worker1" dan type='javascript/worker' (sehingga browser tidak mengurai JS). Kode tersebut diekstrak sebagai string menggunakan document.querySelector('#worker1').textContent dan diteruskan ke Blob() untuk membuat file.

Memuat skrip eksternal

Saat menggunakan teknik ini untuk menyejajarkan kode pekerja, importScripts() hanya akan berfungsi jika Anda memberikan URI absolut. Jika Anda mencoba meneruskan URI relatif, browser akan melaporkan error keamanan. Alasannya adalah: pekerja (kini dibuat dari URL blob) akan di-resolve dengan awalan blob:, sementara aplikasi Anda akan berjalan dari skema yang berbeda (mungkin http://). Oleh karena itu, kegagalan akan disebabkan oleh pembatasan lintas origin.

Salah satu cara untuk menggunakan importScripts() dalam pekerja inline adalah dengan "memasukkan" URL saat ini dari skrip utama yang dijalankan dengan meneruskannya ke pekerja inline dan membuat URL absolut secara manual. Tindakan ini akan memastikan skrip eksternal diimpor dari asal yang sama. Dengan asumsi aplikasi utama Anda berjalan dari http://example.com/index.html:

...
<script id="worker2" type="javascript/worker">
self.onmessage = function(e) {
var data = e.data;

if (data.url) {
var url = data.url.href;
var index = url.indexOf('index.html');
if (index != -1) {
    url = url.substring(0, index);
}
importScripts(url + 'engine.js');
}
...
};
</script>
<script>
var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
worker.postMessage(<b>{url: document.location}</b>);
</script>

menangani error

Seperti halnya logika JavaScript, Anda ingin menangani kesalahan apa pun yang ditunjukkan di pekerja web. Jika terjadi error saat pekerja mengeksekusi, ErrorEvent akan diaktifkan. Antarmuka berisi tiga properti yang berguna untuk mencari tahu apa yang salah: filename - nama skrip pekerja yang menyebabkan error, lineno - nomor baris tempat error terjadi, dan message - deskripsi error yang bermakna. Berikut adalah contoh cara menyiapkan pengendali peristiwa onerror untuk mencetak properti error:

<output id="error" style="color: red;"></output>
<output id="result"></output>

<script>
function onError(e) {
document.getElementById('error').textContent = [
    'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
].join('');
}

function onMsg(e) {
document.getElementById('result').textContent = e.data;
}

var worker = new Worker('workerWithError.js');
worker.addEventListener('message', onMsg, false);
worker.addEventListener('error', onError, false);
worker.postMessage(); // Start worker without a message.
</script>

Contoh: workerWithError.js mencoba menjalankan 1/x, dengan x tidak ditentukan.

// TODO: DevSite - Contoh kode dihapus karena menggunakan pengendali peristiwa inline

workerWithError.js:

self.addEventListener('message', function(e) {
postMessage(1/x); // Intentional error.
};

Sedikit info tentang keamanan

Batasan dengan akses lokal

Karena pembatasan keamanan Google Chrome, pekerja tidak akan berjalan secara lokal (misalnya dari file://) dalam versi browser terbaru. Sebaliknya, mereka gagal diam-diam! Untuk menjalankan aplikasi dari skema file://, jalankan Chrome dengan flag --allow-file-access-from-files yang ditetapkan.

Browser lain tidak menerapkan pembatasan yang sama.

Pertimbangan asal yang sama

Skrip pekerja harus berupa file eksternal dengan skema yang sama dengan halaman panggilannya. Oleh karena itu, Anda tidak dapat memuat skrip dari URL data: atau URL javascript:, dan halaman https: tidak dapat memulai skrip pekerja yang dimulai dengan URL http:.

Kasus penggunaan

Jadi aplikasi jenis apa yang akan memanfaatkan pekerja web? Berikut ini beberapa ide lainnya untuk mengasah otak:

  • Pengambilan data dan/atau cache data untuk digunakan nanti.
  • Penyorotan sintaksis kode atau pemformatan teks real-time lainnya.
  • Pemeriksa ejaan.
  • Menganalisis data video atau audio.
  • I/O latar belakang atau polling layanan web.
  • Memproses array besar atau respons JSON humungous.
  • Pemfilteran gambar di <canvas>.
  • Memperbarui banyak baris database web lokal.

Untuk informasi selengkapnya tentang kasus penggunaan yang melibatkan Web Workers API, kunjungi Ringkasan Pekerja.

Demo

Referensi