Ringkasan Utama
Pindahkan JavaScript sinkron apa pun yang berjalan lebih lama dari satu frame 16ms keluar dari main thread ke dalam Web Worker. Bungkus worker itu dengan Comlink agar pemanggilannya terlihat seperti fungsi async biasa alih-alih pipa postMessage. Pilih worker sebelum round-trip server ketika datanya sudah ada di browser dan biayanya adalah CPU, bukan jaringan.
Setiap aplikasi React punya satu thread yang menggambar UI, menjalankan event handler, dan mengeksekusi render. Ketika satu fungsi di thread itu berjalan selama 200ms, halaman membeku selama 200ms: klik menumpuk, animasi tersendat, dan skor Interaction to Next Paint yang sudah Anda atur dengan cermat jatuh bebas. Saya mengalaminya pada dashboard padat data di mana mem-parsing dan membentuk ulang CSV besar di browser membekukan tab hampir setengah detik pada setiap unggahan.
Solusinya tidak selalu berupa algoritma yang lebih cepat atau endpoint backend. Kadang pekerjaan itu memang milik browser, dan langkah yang tepat adalah menjalankannya di thread lain. Itulah yang diberikan Web Worker, dan Comlink membuatnya nyaris tak terlihat. Berikut cara saya memutuskan kapan menggunakannya, cara memasangnya dalam build Vite, dan jebakan yang diam-diam merenggut frame Anda.
Anggaran 16ms, dan mengapa main thread itu keramat
Pada 60fps browser punya sekitar 16 milidetik untuk memproduksi setiap frame. Dalam jendela itu ia harus menjalankan JavaScript yang tertunda, menghitung ulang style dan layout, lalu menggambar. Apa pun yang kode Anda lakukan secara sinkron memakan anggaran itu. Lewati batasnya dan Anda kehilangan satu frame; jalankan loop 200ms dan Anda kehilangan selusin frame berturut-turut sementara UI membeku. Karena Interaction to Next Paint kini menjadi Core Web Vital, dan skor bagus berarti 200ms atau kurang pada persentil ke-75 dari interaksi nyata, tugas panjang ini bukan lagi sekadar gangguan, melainkan sinyal peringkat.
Web Worker adalah thread JavaScript terpisah dengan event loop-nya sendiri. Ia tidak bisa menyentuh DOM dan tidak berbagi memori dengan komponen Anda, tetapi ia bisa berjalan selama yang dibutuhkan tanpa memblokir satu frame pun di main thread. Main thread tetap bebas untuk menggambar dan merespons sementara pekerjaan berat terjadi di tempat lain.
Jangan letakkan worker di depan pekerjaan murah. Jika tugas selesai dalam kurang dari satu frame, biaya menserialisasi argumen, mengirim pesan, dan mendeserialisasi hasil biasanya lebih besar daripada keuntungannya. Aturan praktis saya: profil dulu, dan hanya gunakan worker ketika satu pemanggilan sinkron secara konsisten melampaui 16ms.
Comlink: RPC yang menyembunyikan postMessage
Worker mentah berkomunikasi melalui postMessage dan handler onmessage. Anda mengirim pesan, mencocokkan respons secara manual, dan akhirnya membangun protokol kecil setiap kali. Comlink, pustaka berukuran sekitar satu kilobyte dari Google Chrome Labs, menggantikan semua itu dengan ES6 Proxy. Anda mengekspos sebuah objek dari worker, membungkus worker di sisi utama, lalu memanggil metodenya seolah-olah fungsi async lokal. Proxy mengubah setiap panggilan menjadi pesan di balik layar dan mengembalikan sebuah promise kepada Anda.
Berikut contoh lengkap dalam proyek Vite plus TypeScript. Worker mengekspos fungsi berat; main thread memanggilnya dan tidak pernah terblokir. Perhatikan pola instansiasi worker, yang Vite butuhkan dalam kondisi utuh untuk membundel worker sebagai chunk terpisah.
// heavy.worker.ts — runs on its own thread
import * as Comlink from 'comlink';
const api = {
// Simulated CPU-bound work: parse + aggregate a large dataset
aggregate(rows: number[]): { sum: number; mean: number } {
let sum = 0;
for (let i = 0; i < rows.length; i++) sum += rows[i];
return { sum, mean: sum / rows.length };
},
};
export type HeavyApi = typeof api;
Comlink.expose(api);// useHeavyWorker.ts — main-thread hook
import * as Comlink from 'comlink';
import { useEffect, useMemo } from 'react';
import type { HeavyApi } from './heavy.worker';
export function useHeavyWorker() {
const worker = useMemo(
() =>
new Worker(new URL('./heavy.worker.ts', import.meta.url), {
type: 'module',
}),
[],
);
const api = useMemo(() => Comlink.wrap<HeavyApi>(worker), [worker]);
// Terminate on unmount so you do not leak threads
useEffect(() => () => worker.terminate(), [worker]);
return api;
}// Component.tsx — the call site looks synchronous, but never blocks
const api = useHeavyWorker();
async function onUpload(rows: number[]) {
// UI stays at 60fps while this runs on the worker thread
const result = await api.aggregate(rows);
setStats(result);
}Kapan worker mengalahkan round-trip server
Naluri ketika sesuatu lambat di browser adalah mendorongnya ke backend. Itu benar ketika pekerjaan butuh database, rahasia, atau state bersama. Itu salah ketika datanya sudah ada di browser dan satu-satunya biaya adalah CPU. Mengirim array besar ke server, menunggu jaringan, dan mem-parsing respons bisa dengan mudah memakan waktu lebih lama daripada komputasinya sendiri, dan itu membakar bandwidth serta satu request server untuk pekerjaan yang bisa dilakukan mesin pengguna secara gratis.
Worker menjaga data tetap lokal, menghindari latensi round-trip sepenuhnya, bekerja offline, dan berskala dengan perangkat pengguna alih-alih anggaran server Anda. Tabel di bawah adalah keputusan yang benar-benar saya buat.
| Faktor | Web Worker | Round-trip server |
|---|---|---|
| Di mana biayanya | Murni CPU, data sudah di browser | Butuh DB, rahasia, atau state bersama |
| Latensi | Tanpa lompatan jaringan | Round-trip plus antrean |
| Offline | Bekerja penuh offline | Gagal tanpa konektivitas |
| Biaya penskalaan | Memakai CPU pengguna, gratis bagi Anda | Menghabiskan kapasitas server |
| Batas kepercayaan | Sisi klien, jangan percaya hasilnya | Otoritatif, divalidasi sisi server |
Worker berjalan di klien, jadi keluarannya tidak pernah otoritatif. Memfilter, mengurutkan, dan pratinjau dalam worker tidak masalah, tetapi apa pun yang mengatur uang, izin, atau state persisten tetap harus divalidasi di server. Perlakukan worker sebagai akselerator untuk UI, bukan pengganti kepercayaan backend.
Jebakan: structured clone, kebocoran, dan bundling
Kejutan paling umum adalah biaya serialisasi. Argumen dan nilai kembalian melintasi batas thread lewat algoritma structured clone, yang menyalin data secara mendalam. Untuk typed array besar, salinan itu sendiri bisa menghabiskan anggaran frame Anda, jadi Anda sudah menyerahkan komputasi tetapi memperkenalkan kembali kemacetan pada salinan. Solusinya adalah transferable object: serahkan kepemilikan ArrayBuffer ke worker alih-alih menyalinnya. Comlink mengeksposnya melalui helper transfer.
import * as Comlink from 'comlink';
// Transfer the underlying buffer instead of cloning it.
// After transfer, `bytes` is neutered on the main thread — do not reuse it.
const bytes = new Uint8Array(hugeArrayBuffer);
await api.process(Comlink.transfer(bytes, [bytes.buffer]));- Membocorkan thread: worker yang dibuat per render tanpa pembersihan menumpuk thread. Buat sekali (useMemo atau singleton modul) dan panggil terminate saat unmount.
- Bundling: dengan Vite, pertahankan pola new Worker dengan new URL dan import.meta.url secara persis dan berikan type module, atau worker tidak akan dibundel sebagai chunk-nya sendiri.
- Tanpa DOM di worker: ia tidak bisa membaca dokumen atau menyentuh state React. Kembalikan data biasa dan biarkan main thread merendernya.
- Cold start: menyalakan worker memakan beberapa milidetik. Untuk panggilan sering, jaga satu worker tetap hidup alih-alih membuat satu per tugas.
Default yang pragmatis
Saya tidak menggunakan worker di setiap proyek. Sebagian besar jank React diperbaiki dengan memoisasi, virtualisasi, atau sekadar melakukan lebih sedikit pekerjaan per render. Tetapi ketika profiling menunjukkan tugas CPU-bound sejati yang harus berjalan di browser, worker yang dibungkus Comlink adalah alat terbersih yang saya kenal: call site-nya terbaca seperti kode async biasa, pekerjaan berat meninggalkan main thread, dan UI menahan 60fps sepanjang operasi. Kombinasi itulah yang menjaga Interaction to Next Paint tetap hijau di bawah beban nyata.

