Arsitektur di luar thread utama dapat meningkatkan keandalan dan pengalaman pengguna aplikasi Anda secara signifikan.
Dalam 20 tahun terakhir, web telah berkembang secara dramatis dari dokumen statis dengan beberapa gaya dan gambar menjadi aplikasi dinamis yang kompleks. Namun, ada satu hal yang sebagian besar tidak berubah: kita hanya memiliki satu thread per tab browser (dengan beberapa pengecualian) untuk melakukan pekerjaan merender situs dan menjalankan JavaScript.
Akibatnya, thread utama menjadi sangat kelebihan beban. Seiring dengan bertambahnya kompleksitas aplikasi web, thread utama menjadi hambatan signifikan bagi performa. Lebih buruk lagi, jumlah waktu yang diperlukan untuk menjalankan kode di thread utama bagi pengguna tertentu hampir sepenuhnya tidak dapat diprediksi karena kemampuan perangkat memiliki pengaruh besar terhadap performa. Ketidakpastian tersebut hanya akan bertambah seiring pengguna mengakses web dari serangkaian perangkat yang semakin beragam, mulai dari ponsel menengah yang sangat terbatas hingga perangkat andalan berdaya tinggi dengan kecepatan refresh tinggi.
Jika kita ingin aplikasi web canggih memenuhi pedoman performa secara andal seperti Core Web Vitals—yang didasarkan pada data empiris tentang persepsi dan psikologi manusia—kita memerlukan cara untuk mengeksekusi kode di luar thread utama (OMT).
Mengapa menggunakan pekerja web?
JavaScript secara default adalah bahasa ber-thread tunggal yang menjalankan tugas di thread utama. Namun, pekerja web menyediakan semacam jalan keluar dari thread utama dengan memungkinkan developer membuat thread terpisah untuk menangani tugas di luar 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 jika tidak, akan membebani thread utama.
Terkait Core Web Vitals, menjalankan tugas di luar thread utama dapat bermanfaat. Khususnya, memindahkan tugas dari thread utama ke pekerja web dapat mengurangi pertentangan untuk thread utama, yang dapat meningkatkan metrik respons Interaction to Next Paint (INP) halaman. Jika thread utama memiliki lebih sedikit tugas yang harus diproses, thread tersebut dapat merespons interaksi pengguna dengan lebih cepat.
Lebih sedikit pekerjaan thread utama—terutama selama startup—juga berpotensi memberikan manfaat bagi Largest Contentful Paint (LCP) dengan mengurangi tugas yang panjang. Merender elemen LCP memerlukan waktu thread utama—baik untuk merender teks atau gambar, yang merupakan elemen LCP yang sering dan umum—dan dengan mengurangi tugas thread utama secara keseluruhan, Anda dapat memastikan bahwa elemen LCP halaman Anda cenderung tidak diblokir oleh tugas berat yang dapat ditangani oleh pekerja web.
Threading dengan pekerja web
Platform lain biasanya mendukung pekerjaan paralel dengan memungkinkan Anda memberikan fungsi ke thread, yang berjalan secara paralel dengan program Anda lainnya. Anda dapat mengakses variabel yang sama dari kedua thread, dan akses ke resource bersama ini dapat disinkronkan dengan mutex dan semaphore untuk mencegah kondisi persaingan.
Di JavaScript, kita bisa mendapatkan fungsi yang hampir serupa dari pekerja web, yang sudah ada sejak tahun 2007 dan didukung di semua browser utama sejak tahun 2012. Web worker berjalan secara paralel dengan thread utama, tetapi tidak seperti threading OS, web worker tidak dapat berbagi variabel.
Untuk membuat web worker, teruskan file ke konstruktor worker, yang akan 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 API postMessage 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. Sebelumnya, pekerja web terutama digunakan untuk memindahkan satu tugas berat dari thread utama. Mencoba menangani beberapa operasi dengan satu pekerja web akan cepat menjadi rumit: Anda harus mengenkode tidak hanya parameter, tetapi juga operasi dalam pesan, dan Anda harus melakukan pembukuan untuk mencocokkan respons dengan permintaan. Kompleksitas tersebut kemungkinan menjadi alasan mengapa pekerja web belum diadopsi secara lebih luas.
Namun, jika kita dapat menghilangkan beberapa kesulitan dalam berkomunikasi antara thread utama dan pekerja web, model ini akan sangat cocok untuk banyak kasus penggunaan. Untungnya, ada library yang melakukan hal itu.
Comlink: membuat pekerja web lebih mudah
Comlink adalah library yang tujuannya adalah memungkinkan Anda menggunakan pekerja web tanpa harus memikirkan detail postMessage. Comlink memungkinkan Anda membagikan variabel antara pekerja web dan thread utama hampir seperti bahasa pemrograman lain yang mendukung threading.
Anda menyiapkan Comlink dengan mengimpornya di web worker dan menentukan serangkaian fungsi untuk diekspos ke thread utama. Kemudian, Anda mengimpor Comlink di thread utama, membungkus 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 di web worker, kecuali setiap fungsi menampilkan promise untuk nilai, bukan nilai itu sendiri.
Kode apa yang harus Anda pindahkan ke pekerja web?
Web worker tidak memiliki akses ke DOM dan banyak API seperti WebUSB, WebRTC, atau Web Audio, sehingga Anda tidak dapat menempatkan bagian aplikasi yang mengandalkan akses tersebut di worker. Namun, setiap bagian kecil kode yang dipindahkan ke pekerja akan memberikan lebih banyak ruang di thread utama untuk hal-hal yang harus ada di sana, seperti memperbarui antarmuka pengguna.
Salah satu masalah bagi developer web adalah sebagian besar aplikasi web mengandalkan framework UI seperti Vue atau React untuk mengatur semuanya di aplikasi; semuanya adalah komponen framework dan secara inheren terikat dengan DOM. Hal ini tampaknya akan menyulitkan migrasi ke arsitektur OMT.
Namun, jika kita beralih ke model yang memisahkan masalah UI dari masalah lainnya, seperti pengelolaan status, web worker dapat sangat berguna bahkan dengan aplikasi berbasis framework. Itulah pendekatan yang dilakukan dengan PROXX.
PROXX: studi kasus OMT
Tim Google Chrome mengembangkan PROXX sebagai tiruan Minesweeper yang memenuhi persyaratan Progressive Web App, termasuk dapat digunakan secara offline dan memiliki pengalaman pengguna yang menarik. Sayangnya, versi awal game berperforma buruk di perangkat terbatas seperti ponsel fitur, yang membuat tim menyadari bahwa thread utama adalah hambatan.
Tim memutuskan untuk menggunakan pekerja web guna memisahkan status visual game dari logikanya:
- Thread utama menangani rendering animasi dan transisi.
- Web worker menangani logika game, yang murni komputasional.
OMT memiliki efek menarik pada performa ponsel fitur PROXX. Pada versi non-OMT, UI akan berhenti berfungsi selama enam detik setelah pengguna berinteraksi dengannya. Tidak ada masukan, dan pengguna harus menunggu selama enam detik penuh sebelum dapat melakukan hal lain.
Namun, pada versi OMT, game membutuhkan waktu dua belas detik untuk menyelesaikan update UI. Meskipun tampak seperti penurunan performa, hal ini justru meningkatkan masukan kepada 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 diperbarui, sehingga membuat game terasa jauh lebih baik.
Ini adalah pertukaran yang disadari: kami memberikan pengalaman yang terasa lebih baik kepada pengguna perangkat dengan batasan tanpa menghukum pengguna perangkat kelas atas.
Implikasi arsitektur OMT
Seperti yang ditunjukkan contoh PROXX, OMT membuat aplikasi Anda berjalan dengan andal di berbagai perangkat, tetapi tidak membuat aplikasi Anda lebih cepat:
- Anda hanya memindahkan pekerjaan dari thread utama, bukan mengurangi pekerjaan.
- Overhead komunikasi tambahan antara pekerja web dan thread utama terkadang dapat membuat proses sedikit lebih lambat.
Pertimbangkan konsekuensinya
Karena thread utama bebas memproses interaksi pengguna seperti men-scroll saat JavaScript berjalan, frame yang terputus lebih sedikit meskipun total waktu tunggu mungkin sedikit lebih lama. Membuat pengguna menunggu sebentar lebih baik daripada menjatuhkan frame karena margin error lebih kecil untuk frame yang dijatuhkan: menjatuhkan frame terjadi dalam milidetik, sementara Anda memiliki ratusan milidetik sebelum pengguna merasakan waktu tunggu.
Karena performa di berbagai perangkat tidak dapat diprediksi, tujuan arsitektur OMT adalah mengurangi risiko—membuat aplikasi Anda lebih andal dalam menghadapi kondisi runtime yang sangat bervariasi—bukan tentang manfaat performa dari paralelisme. Peningkatan ketahanan dan UX lebih berharga daripada sedikit penurunan kecepatan.
Catatan tentang alat
Web worker belum populer, sehingga sebagian besar alat modul—seperti webpack dan Rollup—tidak mendukungnya secara langsung. (Parcel bisa melakukannya!) Untungnya, ada plugin untuk membuat pekerja web, ya, berfungsi dengan webpack dan Rollup:
- worker-plugin untuk webpack
- rollup-plugin-off-main-thread untuk Rollup
Kesimpulan
Untuk memastikan aplikasi kami seandal dan semudah diakses mungkin, terutama di marketplace yang semakin terglobalisasi, kami perlu mendukung perangkat yang memiliki keterbatasan—karena perangkat tersebut adalah cara sebagian besar pengguna mengakses web secara global. OMT menawarkan cara yang menjanjikan untuk meningkatkan performa di perangkat tersebut tanpa memengaruhi pengguna perangkat kelas atas secara negatif.
Selain itu, OMT memiliki manfaat sekunder:
- Hal ini memindahkan biaya eksekusi JavaScript ke thread terpisah.
- Hal ini memindahkan biaya parsing, yang berarti UI dapat di-boot lebih cepat. Hal ini dapat mengurangi First Contentful Paint atau bahkan Time to Interactive, yang pada gilirannya dapat meningkatkan skor Lighthouse Anda.
Web worker tidak harus menakutkan. Alat seperti Comlink menghilangkan pekerjaan pekerja dan menjadikannya pilihan yang layak untuk berbagai aplikasi web.