Menggunakan pekerja web untuk menjalankan JavaScript di luar thread utama browser

Arsitektur di luar thread utama dapat meningkatkan keandalan dan pengalaman pengguna aplikasi Anda secara signifikan.

Dalam 20 tahun terakhir, web telah berevolusi secara dramatis dari dokumen statis dengan beberapa gaya dan gambar menjadi aplikasi yang kompleks dan dinamis. Namun, satu hal sebagian besar tidak berubah: kita hanya memiliki satu utas per tab browser (dengan beberapa pengecualian) untuk melakukan pekerjaan merender situs dan menjalankan JavaScript kami.

Akibatnya, thread utama menjadi sangat kewalahan. Dan seiring aplikasi web bertambah kompleks, thread utama menjadi bottleneck yang signifikan untuk performa. Lebih buruk lagi, jumlah waktu yang diperlukan untuk menjalankan kode pada thread utama untuk pengguna tertentu hampir sepenuhnya tidak dapat diprediksi karena kemampuan perangkat memiliki pengaruh besar pada performa. Ketidakpastian tersebut akan semakin besar seiring pengguna mengakses web dari kumpulan perangkat yang semakin beragam, mulai dari ponsel berfitur yang sangat terbatas hingga mesin flagship yang memiliki daya tinggi dan memiliki kecepatan refresh tinggi.

Jika kita ingin aplikasi web canggih memenuhi panduan performa seperti Core Web Vitals—yang didasarkan pada data empiris tentang persepsi dan psikologi manusia—kita memerlukan cara untuk menjalankan kode dari thread utama (OMT).

Mengapa pekerja web?

Secara default, JavaScript adalah bahasa thread tunggal yang menjalankan tugas di thread utama. Namun, pekerja web menyediakan semacam jalan keluar dari thread utama dengan mengizinkan developer membuat thread terpisah untuk menangani pekerjaan dari thread utama. Meskipun cakupan pekerja web terbatas dan tidak menawarkan akses langsung ke DOM, pekerja web dapat sangat bermanfaat jika ada banyak pekerjaan yang perlu dilakukan yang akan membebani thread utama.

Untuk masalah Core Web Vitals, menjalankan tugas di luar thread utama dapat bermanfaat. Secara khusus, mengalihkan pekerjaan dari thread utama ke pekerja web dapat mengurangi pertentangan untuk thread utama, yang dapat meningkatkan metrik responsivitas Interaction to Next Paint (INP) halaman. Jika memiliki lebih sedikit tugas untuk diproses, thread utama dapat merespons interaksi pengguna dengan lebih cepat.

Lebih sedikit pekerjaan thread utama—terutama selama startup—juga membawa potensi manfaat untuk Largest Contentful Paint (LCP) dengan mengurangi tugas yang panjang. Merender elemen LCP memerlukan waktu thread utama—baik untuk merender teks maupun gambar, yang merupakan elemen LCP yang umum dan sering—dan dengan mengurangi pekerjaan thread utama secara keseluruhan, Anda dapat memastikan bahwa elemen LCP halaman Anda tidak akan terhalang oleh pekerjaan mahal yang dapat ditangani oleh pekerja web.

Threading dengan pekerja web

Platform lain biasanya mendukung pekerjaan paralel dengan mengizinkan Anda memberikan fungsi pada thread, yang berjalan secara paralel dengan bagian program lainnya. Anda dapat mengakses variabel yang sama dari kedua utas, dan akses ke sumber daya bersama ini dapat disinkronkan dengan mutex dan semafora untuk mencegah kondisi race.

Di JavaScript, kita bisa mendapatkan fungsionalitas yang kurang lebih serupa dari pekerja web, yang telah ada sejak 2007 dan didukung di semua browser utama sejak 2012. Pekerja web berjalan secara paralel dengan thread utama, tetapi tidak seperti threading OS, mereka tidak dapat berbagi variabel.

Untuk membuat pekerja web, teruskan file ke konstruktor pekerja, yang mulai menjalankan file tersebut dalam thread terpisah:

const worker = new Worker("./worker.js");

Berkomunikasi dengan pekerja web dengan mengirim pesan menggunakan postMessage API. Teruskan nilai pesan sebagai parameter dalam panggilan postMessage, lalu tambahkan pemroses peristiwa pesan ke pekerja:

main.js

const worker = new Worker('./worker.js');
worker.postMessage([40, 2]);

worker.js

addEventListener('message', event => {
  const [a, b] = event.data;

  // Do stuff with the message
  // ...
});

Untuk mengirim pesan kembali ke thread utama, gunakan postMessage API yang sama di pekerja web dan siapkan pemroses peristiwa di thread utama:

main.js

const worker = new Worker('./worker.js');

worker.postMessage([40, 2]);
worker.addEventListener('message', event => {
  console.log(event.data);
});

worker.js

addEventListener('message', event => {
  const [a, b] = event.data;

  // Do stuff with the message
  postMessage(a + b);
});

Harus diakui, pendekatan ini agak terbatas. Sebelumnya, pekerja web umumnya digunakan untuk memindahkan satu pekerjaan berat dari thread utama. Mencoba menangani beberapa operasi dengan satu pekerja web akan menjadi sulit dengan cepat: Anda tidak hanya harus mengenkode parameter, tetapi juga operasi dalam pesan, dan Anda harus melakukan pembukuan untuk mencocokkan respons terhadap permintaan. Kompleksitas tersebut mungkin menjadi alasan mengapa pekerja web belum diadopsi secara lebih luas.

Namun, jika kita dapat menghilangkan sebagian kesulitan komunikasi antara thread utama dan pekerja web, model ini mungkin sangat cocok untuk berbagai kasus penggunaan. Untungnya, ada perpustakaan yang bisa melakukan hal itu.

Comlink adalah library yang tujuannya adalah memungkinkan Anda menggunakan pekerja web tanpa harus memikirkan detail postMessage. Comlink memungkinkan Anda berbagi variabel antara pekerja web dan thread utama, hampir seperti bahasa pemrograman lain yang mendukung threading.

Anda menyiapkan Comlink dengan mengimpornya di pekerja web dan menentukan serangkaian fungsi yang akan diekspos ke thread utama. Kemudian, impor Comlink di thread utama, gabungkan pekerja, dan dapatkan akses ke fungsi yang diekspos:

worker.js

import {expose} from 'comlink';

const api = {
  someMethod() {
    // ...
  }
}

expose(api);

main.js

import {wrap} from 'comlink';

const worker = new Worker('./worker.js');
const api = wrap(worker);

Variabel api pada thread utama berperilaku sama seperti yang ada di pekerja web, kecuali bahwa setiap fungsi menampilkan promise untuk sebuah nilai, bukan nilai itu sendiri.

Kode apa yang harus Anda pindahkan ke pekerja web?

Pekerja web tidak memiliki akses ke DOM dan banyak API seperti WebUSB, WebRTC, atau Audio Web, sehingga Anda tidak dapat menempatkan bagian aplikasi yang mengandalkan akses tersebut di pekerja. Namun, setiap potongan kecil kode yang dipindahkan ke pekerja akan membeli lebih banyak headroom di thread utama untuk hal-hal yang harus ada, seperti mengupdate antarmuka pengguna.

Satu masalah bagi developer web adalah sebagian besar aplikasi web mengandalkan framework UI seperti Vue atau React untuk mengatur semua yang ada di aplikasi; semuanya merupakan komponen framework sehingga terkait erat dengan DOM. Hal ini tampaknya akan menyulitkan migrasi ke arsitektur OMT.

Namun, jika kita beralih ke model yang memisahkan masalah UI dari masalah lain, seperti pengelolaan status, pekerja web dapat sangat berguna bahkan dengan aplikasi berbasis framework. Itulah pendekatan yang diambil dengan PROXX.

PROXX: studi kasus OMT

Tim Google Chrome mengembangkan PROXX sebagai clone Minesweeper yang memenuhi persyaratan Progressive Web App, termasuk bekerja secara offline dan memiliki pengalaman pengguna yang menarik. Sayangnya, versi awal game ini berperforma buruk pada perangkat terbatas seperti ponsel menengah, yang membuat tim menyadari bahwa thread utama merupakan bottleneck.

Tim memutuskan untuk menggunakan pekerja web untuk memisahkan status visual game dari logikanya:

  • Thread utama menangani rendering animasi dan transisi.
  • Pekerja web menangani logika game, yang sepenuhnya bersifat komputasi.

OMT memiliki efek yang menarik pada performa ponsel menengah PROXX. Pada versi non-OMT, UI dibekukan selama enam detik setelah pengguna berinteraksi dengannya. Tidak ada masukan, dan pengguna harus menunggu selama enam detik penuh sebelum dapat melakukan hal lain.

Waktu respons UI di PROXX versi non-OMT.

Namun, dalam versi OMT, game memerlukan dua belas detik untuk menyelesaikan update UI. Meskipun tampaknya seperti penurunan performa, hal ini sebenarnya menyebabkan peningkatan masukan bagi pengguna. Pelambatan terjadi karena aplikasi mengirimkan lebih banyak frame daripada versi non-OMT, yang tidak mengirimkan frame sama sekali. Oleh karena itu, pengguna tahu bahwa sesuatu sedang terjadi dan dapat terus bermain saat UI diupdate, sehingga membuat game terasa jauh lebih baik.

Waktu respons UI di PROXX versi OMT.

Ini merupakan kompromi yang disengaja: kami memberikan pengalaman kepada pengguna perangkat terbatas yang merasa lebih baik tanpa menghukum pengguna perangkat kelas atas.

Implikasi arsitektur OMT

Seperti yang ditunjukkan contoh PROXX, OMT membuat aplikasi Anda dapat berjalan dengan andal di berbagai perangkat, tetapi tidak mempercepat aplikasi Anda:

  • Anda hanya memindahkan pekerjaan dari thread utama, tidak mengurangi pekerjaan.
  • Overhead komunikasi tambahan antara pekerja web dan thread utama terkadang dapat membuat sedikit lebih lambat.

Pertimbangkan konsekuensinya

Karena thread utama bebas memproses interaksi pengguna seperti men-scroll saat JavaScript berjalan, akan ada lebih sedikit frame yang dihapus meskipun total waktu tunggu mungkin sedikit lebih lama. Membuat pengguna menunggu sedikit lebih baik daripada menghapus frame karena margin kesalahan lebih kecil untuk frame yang dihapus: penurunan frame terjadi dalam milidetik, sementara Anda memiliki ratusan milidetik sebelum pengguna melihat waktu tunggu.

Karena performa yang tidak dapat diprediksi di berbagai perangkat, sasaran arsitektur OMT sebenarnya adalah mengurangi risiko—membuat aplikasi Anda lebih tangguh dalam menghadapi kondisi runtime yang sangat bervariasi—bukan tentang manfaat performa dari paralelisasi. Peningkatan ketahanan dan peningkatan pada UX lebih dari sekadar kompromi kecil dalam hal kecepatan.

Catatan tentang alat

Pekerja web belum menjadi layanan umum, jadi sebagian besar alat modul—seperti webpack dan Rollup—tidak mendukungnya secara langsung. (Namun, Parcel berhasil!) Untungnya, ada plugin untuk membuat web worker bekerja dengan webpack dan Rollup:

Mengambil kesimpulan

Untuk memastikan aplikasi kami dapat diandalkan dan dapat diakses sebanyak mungkin, terutama di pasar yang semakin global, kami perlu mendukung perangkat yang terbatas—begitulah cara sebagian besar pengguna mengakses web secara global. OMT menawarkan cara yang menjanjikan untuk meningkatkan performa pada perangkat tersebut tanpa berpengaruh buruk terhadap pengguna perangkat kelas atas.

Selain itu, OMT memiliki manfaat sekunder:

  • Ini memindahkan biaya eksekusi JavaScript ke thread terpisah.
  • API ini memindahkan biaya penguraian, yang berarti UI mungkin melakukan booting lebih cepat. Hal ini dapat mengurangi First Contentful Paint atau bahkan Time to Interactive, yang pada akhirnya dapat meningkatkan Skor Lighthouse.

Pekerja web tidak perlu merasa menakutkan. Alat seperti Comlink menghilangkan pekerjaan pekerja dan menjadikannya pilihan yang tepat untuk berbagai aplikasi web.

Banner besar dari Unsplash, oleh James Peacock.