Menggunakan pekerja web untuk menjalankan JavaScript di luar thread utama browser

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

Surma
Surma

Dalam 20 tahun terakhir, web telah berkembang secara dramatis dari dokumen statis dengan beberapa gaya dan gambar hingga aplikasi yang dinamis dan kompleks. Namun, ada satu hal yang sebagian besar tetap tidak berubah: kami hanya memiliki satu thread per tab browser (dengan beberapa pengecualian) untuk melakukan rendering situs dan menjalankan JavaScript kami.

Akibatnya, thread utama menjadi sangat kewalahan bekerja. Dan seiring aplikasi web yang semakin kompleks, thread utama menjadi bottleneck yang signifikan bagi performa. Untuk memperburuk masalah, jumlah waktu yang diperlukan untuk menjalankan kode di thread utama untuk pengguna tertentu hampir sama sekali tidak dapat diprediksi karena kemampuan perangkat memiliki pengaruh yang besar pada performa. Ketidakpastian tersebut akan bertambah besar seiring pengguna mengakses web dari berbagai perangkat yang semakin beragam, mulai dari ponsel menengah yang sangat terbatas hingga mesin unggulan yang menggunakan teknologi tinggi dan memiliki kecepatan refresh tinggi.

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

Mengapa pekerja web?

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

Jika Data Web Inti berkaitan, menjalankan pekerjaan di luar thread utama dapat bermanfaat. Secara khusus, mengurangi beban pekerjaan dari thread utama ke pekerja web dapat mengurangi pertentangan untuk thread utama, yang dapat meningkatkan metrik responsivitas penting seperti Interaction to Next Paint (INP) dan Penundaan Input Pertama (FID). Jika thread utama memiliki lebih sedikit pekerjaan untuk diproses, thread utama dapat merespons interaksi pengguna dengan lebih cepat.

Pekerjaan thread utama yang lebih sedikit—terutama selama startup—juga memberikan potensi manfaat untuk Largest Contentful Paint (LCP) dengan mengurangi tugas yang berjalan lama. Merender elemen LCP memerlukan waktu thread utama—baik untuk merender teks atau gambar, yang merupakan elemen LCP yang sering dan umum—dan dengan mengurangi pekerjaan thread utama secara keseluruhan, Anda dapat memastikan bahwa elemen LCP di halaman Anda cenderung tidak terhalang oleh pekerjaan mahal yang dapat ditangani oleh pekerja web.

Threading dengan pekerja web

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

Di JavaScript, kita bisa mendapatkan fungsi serupa dari web worker, 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 di thread terpisah:

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

Berkomunikasi dengan pekerja web dengan mengirim pesan melalui 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);
});

Memang, pendekatan ini agak terbatas. Selama ini, pekerja web lebih banyak digunakan untuk memindahkan satu bagian pekerjaan berat dari thread utama. Mencoba menangani beberapa operasi dengan satu pekerja web menjadi sulit dengan cepat: Anda harus mengenkode parameter tetapi juga operasi dalam pesan, dan Anda harus melakukan pembukuan untuk mencocokkan respons dengan permintaan. Kompleksitas itu kemungkinan menjadi penyebab pekerja web belum diadopsi secara luas.

Namun, jika kita dapat menghilangkan kesulitan berkomunikasi antara thread utama dan pekerja web, model ini sangat cocok untuk banyak kasus penggunaan. Dan, untungnya, ada library yang 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 dapat menyiapkan Comlink dengan mengimpornya di pekerja web dan menentukan serangkaian fungsi yang akan diekspos ke thread utama. Kemudian, Anda akan mengimpor Comlink di thread utama, menggabungkan pekerja, dan mendapatkan 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 di thread utama berperilaku sama seperti yang ada di pekerja web, kecuali bahwa setiap fungsi menampilkan promise untuk 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 bergantung pada akses tersebut di pekerja. Namun, setiap potongan kecil kode yang dipindahkan ke pekerja membeli lebih banyak kapasitas di thread utama untuk hal-hal yang harus ada di sana—seperti memperbarui antarmuka pengguna.

Satu masalah bagi developer web adalah sebagian besar aplikasi web mengandalkan framework UI seperti Vue atau React untuk mengatur segala sesuatu di aplikasi; semuanya merupakan komponen framework, dan begitu pula terikat dengan DOM. Hal tersebut tampaknya akan menyulitkan migrasi ke arsitektur OMT.

Namun, jika kita beralih ke model yang tidak mementingkan masalah UI, tetapi juga berfokus pada masalah lainnya, seperti pengelolaan status, pekerja web bisa 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 berperforma buruk pada perangkat yang 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 hanya bersifat komputasional.

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

Waktu respons UI dalam PROXX versi non-OMT.

Namun, dalam versi OMT, game membutuhkan waktu dua belas detik untuk menyelesaikan update UI. Meskipun terlihat seperti penurunan performa, sebenarnya hal itu menimbulkan 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 seiring update UI, membuat game terasa jauh lebih baik.

Waktu respons UI dalam PROXX versi OMT.

Hal ini menimbulkan konsekuensi: kami memberikan pengalaman yang terasa lebih baik kepada pengguna perangkat kelas atas tanpa merugikan pengguna perangkat kelas atas.

Implikasi arsitektur OMT

Seperti yang ditunjukkan contoh PROXX, OMT membuat aplikasi Anda berjalan andal di berbagai perangkat, tetapi tidak membuat aplikasi Anda lebih cepat:

  • Anda hanya memindahkan tugas dari thread utama, bukan mengurangi tugas.
  • Overhead komunikasi tambahan antara pekerja web dan thread utama terkadang dapat membuat semuanya menjadi sedikit lebih lambat.

Mempertimbangkan konsekuensi

Karena thread utama bebas memproses interaksi pengguna seperti men-scroll saat JavaScript berjalan, jumlah frame yang hilang lebih sedikit meskipun total waktu tunggu mungkin sedikit lebih lama. Membuat pengguna menunggu sebentar lebih baik untuk menghilangkan frame karena margin error lebih kecil untuk penurunan frame: penurunan frame terjadi dalam milidetik, sementara Anda memiliki ratusan milidetik sebelum pengguna merasakan waktu tunggu.

Karena ketidakpastian performa di seluruh perangkat, tujuan arsitektur OMT sebenarnya adalah mengurangi risiko—menjadikan aplikasi Anda lebih tangguh dalam menghadapi kondisi runtime yang sangat bervariasi—bukan tentang manfaat performa paralelisasi. Peningkatan ketahanan dan peningkatan pada UX lebih dari sebanding dengan kecepatannya.

Catatan tentang alat

Pekerja web masih belum terlalu umum, jadi sebagian besar alat modul—seperti webpack dan Rollup—tidak langsung mendukung mereka. (Namun, Parcel melakukannya!) Untungnya, ada plugin untuk membuat pekerja web berfungsi dengan webpack dan Rollup:

Mengambil kesimpulan

Untuk memastikan aplikasi kami dapat diandalkan dan dapat diakses semudah mungkin, terutama di marketplace yang semakin global, kita perlu mendukung perangkat yang terbatas—inilah 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:

  • Metode ini memindahkan biaya eksekusi JavaScript ke thread terpisah.
  • Library ini akan memindahkan biaya penguraian, yang berarti UI dapat melakukan booting lebih cepat. Tindakan tersebut dapat mengurangi First Contentful Paint atau bahkan Waktu untuk Interaktif, yang pada akhirnya dapat meningkatkan skor Lighthouse Anda.

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

Hero image dari Unsplash, oleh James Peacock.