Web Workers: Bí kíp xử lý Task nặng mà không làm ‘đứng hình’ giao diện

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Cơn ác mộng “Page Unresponsive” và giới hạn của JavaScript

Bạn đã bao giờ gặp cảnh website đứng hình ngay khi người dùng nhấn nút “Xuất báo cáo”? Màn hình đơ cứng, con trỏ chuột quay vòng và trình duyệt hiện thông báo “Page Unresponsive” đáng ghét. Đây là hệ quả tất yếu khi Main Thread bị quá tải.

Về bản chất, JavaScript chạy đơn luồng (Single-threaded). Mọi việc từ vẽ giao diện (render), bắt sự kiện click đến tính toán logic đều xếp hàng chờ xử lý trên một con đường duy nhất. Nếu bạn bắt nó xử lý mảng 1 triệu phần tử, con đường đó sẽ bị tắc nghẽn. Trình duyệt không còn tài nguyên để cập nhật UI, khiến trải nghiệm người dùng rơi xuống con số 0.

Web Workers chính là giải pháp cứu cánh. Nó cho phép bạn mở thêm một “làn đường ưu tiên” chạy ngầm (background thread). Task nặng cứ việc chạy ở dưới, trong khi Main Thread bên trên vẫn thong dong xử lý các thao tác cuộn trang hay click chuột của người dùng.

Triển khai Web Workers: Tách luồng để giải phóng UI

Tin vui là Web Workers là API có sẵn, bạn không cần cài thêm bất kỳ thư viện nặng nề nào. Tuy nhiên, bạn cần tách code thành các file riêng biệt vì Worker hoạt động trong một môi trường hoàn toàn độc lập.

Trước khi đẩy dữ liệu vào Worker, mình thường dùng JSON Formatter để kiểm tra cấu trúc. Việc này giúp đảm bảo dữ liệu đầu vào chuẩn chỉnh, tránh các lỗi logic ngớ ngẩn khiến Worker bị crash giữa chừng.

Hãy xem cách thiết lập cơ bản giữa file luồng chính (main.js) và file xử lý ngầm (worker.js).

Bước 1: Khởi tạo luồng phụ trong main.js

// main.js
if (window.Worker) {
    const myWorker = new Worker('worker.js');

    // Gửi 500,000 bản ghi xuống xử lý ngầm
    myWorker.postMessage(hugeDataset);

    // Nhận kết quả khi Worker hoàn thành
    myWorker.onmessage = (e) => {
        console.log('Data đã xử lý xong:', e.data);
        renderUI(e.data);
    };

    myWorker.onerror = (err) => console.error('Lỗi Worker:', err.message);
}

Bước 2: Viết logic xử lý trong worker.js

Lưu ý quan trọng: Trong Worker, bạn không thể truy cập DOM (document, window). Mọi thao tác chỉ xoay quanh dữ liệu thuần túy.

// worker.js
onmessage = function(e) {
    const data = e.data;
    
    // Giả lập xử lý nặng: Map qua 1 triệu item
    const result = data.map(item => {
        let sum = 0;
        for(let i = 0; i < 1000; i++) sum += i;
        return { ...item, score: sum };
    });

    postMessage(result);
};

Tối ưu hóa: Tránh bẫy hiệu năng khi truyền dữ liệu

Mặc định, trình duyệt dùng thuật toán Structured Clone để copy dữ liệu giữa các luồng. Nếu bạn gửi một file JSON 100MB, trình duyệt sẽ tốn kha khá CPU và RAM chỉ để tạo ra một bản sao tương tự. Điều này vô tình tạo ra một điểm nghẽn mới.

Dùng Transferable Objects để đạt tốc độ tối đa

Với các dữ liệu dạng mảng byte (ArrayBuffer), hãy dùng cơ chế “chuyển nhượng quyền sở hữu”. Thay vì copy, Main Thread sẽ chuyển thẳng địa chỉ vùng nhớ cho Worker. Sau khi chuyển, Main Thread mất quyền truy cập, nhưng tốc độ truyền tải gần như bằng 0ms.

// Chuyển nhượng 32MB dữ liệu mà không tốn tài nguyên copy
const buffer = new ArrayBuffer(1024 * 1024 * 32);
myWorker.postMessage(buffer, [buffer]);

Kiểm chứng hiệu quả: Con số không biết nói dối

Đừng đoán mò hiệu năng, hãy mở Chrome DevTools để thấy sự khác biệt rõ rệt. Trong tab Performance, bạn sẽ thấy sự thay đổi ngoạn mục trước và sau khi dùng Worker.

  • Không có Worker: Một vạch đỏ dài xuất hiện ở hàng Main, báo hiệu “Long Task” (thường > 50ms). Website sẽ bị khựng lại.
  • Có Worker: Hàng Main trống trải, xanh mướt. Các khối tính toán nặng được đẩy xuống một hàng riêng có tên “Worker”.

Kinh nghiệm thực tế cho thấy, bạn chỉ nên dùng Web Worker cho các tác vụ tốn trên 100ms. Việc khởi tạo Worker cũng tốn một khoảng chi phí (overhead). Nếu chỉ cộng vài con số đơn giản, chạy trực tiếp trên Main Thread đôi khi còn nhanh hơn.

Để quản lý Worker chuyên nghiệp hơn trong các dự án lớn, hãy thử qua Comlink. Thư viện này biến việc giao tiếp postMessage phức tạp thành các lời gọi hàm async/await cực kỳ gọn gàng. Web Worker không chỉ là công cụ, nó là tư duy tách biệt giữa tính toán và hiển thị để tạo ra những ứng dụng web chuẩn chuyên nghiệp.

Share: