async/await giải quyết bài toán chờ, không phải bài toán tính. JavaScript chạy trên một luồng duy nhất: await chỉ tạm dừng hàm và trả quyền điều khiển lại cho event loop, để luồng đó làm việc khác trong lúc I/O (mạng, đĩa) chạy ở tầng dưới. Không có luồng thứ hai nào được sinh ra.
Hệ quả: bọc một vòng lặp nặng CPU vào async không giúp gì — UI vẫn đứng hình.
async function heavy() {
return hashMillionRows(rows) // vẫn chiếm luồng chính, vẫn chặn render
}Web Worker là luồng thật, có bộ nhớ riêng, giao tiếp qua postMessage. Dùng khi tác vụ CPU-bound kéo dài quá vài chục ms:
- Parse/biến đổi JSON hoặc CSV lớn, xử lý ảnh, mã hoá/hash, nén.
- Tính toán trong bảng dữ liệu lớn, diff, tìm kiếm cục bộ.
const worker = new Worker(new URL('./parse.worker.js', import.meta.url), { type: 'module' })
worker.postMessage(file)
worker.onmessage = (e) => render(e.data)Giới hạn cần biết: worker không truy cập DOM; dữ liệu qua postMessage bị structured clone (tốn chi phí copy — dùng Transferable như ArrayBuffer để chuyển quyền sở hữu thay vì copy).
Khi công việc nhẹ hơn, có thể chỉ cần chia nhỏ tác vụ và nhả luồng bằng scheduler.yield()/setTimeout để giữ trang phản hồi, không nhất thiết dựng worker.