Tạo luồng web bằng trình chạy mô-đun

Giờ đây, việc chuyển phần công việc nặng nhọc vào các luồng trong nền nay trở nên dễ dàng hơn nhờ các mô-đun JavaScript trong nhân viên web.

JavaScript là đơn luồng, tức là mỗi lần chỉ có thể thực hiện một thao tác. Đây là giao diện trực quan và hoạt động tốt trong nhiều trường hợp trên web, nhưng có thể trở thành vấn đề khi chúng tôi phải thực hiện các công việc phức tạp như xử lý dữ liệu, phân tích cú pháp, tính toán hoặc phân tích. Khi ngày càng nhiều ứng dụng phức tạp được phân phối trên web, nhu cầu xử lý đa luồng ngày càng tăng.

Trên nền tảng web, nguyên liệu chính để phân luồng và tạo song song là API Trình chạy web. Worker là một yếu tố trừu tượng nhẹ bên trên các luồng hệ điều hành, hiển thị một thông báo truyền API để giao tiếp liên luồng. Điều này có thể cực kỳ hữu ích khi thực hiện các phép tính tốn kém hoặc thao tác trên các tập dữ liệu lớn, giúp luồng chính chạy mượt mà trong khi thực hiện các thao tác tốn kém trên một hoặc nhiều luồng trong nền.

Dưới đây là ví dụ điển hình về việc sử dụng trình thực thi, trong đó tập lệnh trình thực thi theo dõi thông báo từ luồng chính và phản hồi bằng cách gửi lại thông báo của chính trình thực thi đó:

page.js:

const worker = new Worker('worker.js');
worker.addEventListener('message', e => {
  console.log(e.data);
});
worker.postMessage('hello');

worker.js:

addEventListener('message', e => {
  if (e.data === 'hello') {
    postMessage('world');
  }
});

Web Worker API đã có sẵn trong hầu hết các trình duyệt trong hơn mười năm. Mặc dù điều đó có nghĩa là trình thực thi có hỗ trợ trình duyệt tuyệt vời và được tối ưu hoá tốt, nhưng điều đó cũng có nghĩa là chúng đã tạo sẵn các mô-đun JavaScript từ lâu. Vì không có hệ thống mô-đun khi worker được thiết kế, nên API để tải mã vào một worker và soạn tập lệnh vẫn tương tự như các phương pháp tải tập lệnh đồng bộ phổ biến trong năm 2009.

Nhật ký: worker phiên bản cũ

Hàm khởi tạo Worker lấy một URL tập lệnh kiểu cũ tương ứng với URL của tài liệu. API này ngay lập tức trả về tham chiếu đến thực thể worker mới, hiển thị giao diện thông báo cũng như phương thức terminate() ngay lập tức dừng và huỷ bỏ worker đó.

const worker = new Worker('worker.js');

Hàm importScripts() có sẵn trong web worker để tải mã bổ sung, nhưng hàm này tạm dừng thực thi worker để tìm nạp và đánh giá từng tập lệnh. Lệnh này cũng thực thi các tập lệnh trong phạm vi toàn cục như thẻ <script> cổ điển, tức là các biến trong một tập lệnh có thể bị các biến trong tập lệnh khác ghi đè.

worker.js:

importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

// global to the whole worker
function sayHello() {
  return 'world';
}

Vì lý do này, trước đây, trình thực thi web đã áp đặt một hiệu ứng quá lớn đối với kiến trúc của một ứng dụng. Các nhà phát triển đã phải tạo ra những công cụ và giải pháp thông minh để có thể sử dụng trình thực thi web mà không cần từ bỏ các phương pháp phát triển hiện đại. Ví dụ: các trình gói như webpack nhúng một phương thức triển khai trình tải mô-đun nhỏ vào mã được tạo sử dụng importScripts() để tải mã, nhưng bao bọc các mô-đun trong hàm để tránh xung đột biến, đồng thời mô phỏng hoạt động nhập và xuất phần phụ thuộc.

Nhập trình thực thi mô-đun

Một chế độ mới dành cho nhân viên chạy web với lợi ích về hiệu suất và hiệu suất của các mô-đun JavaScript đang được chuyển trong Chrome 80, được gọi là trình thực thi mô-đun. Hàm khởi tạo Worker hiện chấp nhận một tuỳ chọn {type:"module"} mới, tuỳ chọn này thay đổi quá trình tải và thực thi tập lệnh để khớp với <script type="module">.

const worker = new Worker('worker.js', {
  type: 'module'
});

Vì trình chạy mô-đun là các mô-đun JavaScript tiêu chuẩn, nên chúng có thể sử dụng câu lệnh nhập và xuất. Giống như tất cả các mô-đun JavaScript, các phần phụ thuộc chỉ được thực thi một lần trong một ngữ cảnh nhất định (luồng chính, trình thực thi, v.v.) và tất cả các lệnh nhập trong tương lai đều tham chiếu đến bản sao mô-đun đã được thực thi. Việc tải và thực thi các mô-đun JavaScript cũng được các trình duyệt tối ưu hoá. Các phần phụ thuộc của mô-đun có thể được tải trước khi thực thi mô-đun. Điều này cho phép tải song toàn toàn bộ cây mô-đun. Quá trình tải mô-đun cũng lưu mã đã phân tích cú pháp vào bộ nhớ đệm, tức là các mô-đun được dùng trên luồng chính và trong worker chỉ cần được phân tích cú pháp một lần.

Việc chuyển sang các mô-đun JavaScript cũng cho phép sử dụng tính năng nhập động đối với mã tải từng phần mà không chặn quá trình thực thi của worker. Tính năng nhập động sẽ rõ ràng hơn nhiều so với sử dụng importScripts() để tải các phần phụ thuộc, vì dữ liệu xuất của mô-đun đã nhập sẽ được trả về thay vì dựa vào các biến toàn cục.

worker.js:

import { sayHello } from './greet.js';
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

import greetings from './data.js';
export function sayHello() {
  return greetings.hello;
}

Để đảm bảo hiệu suất cao, phương thức importScripts() cũ sẽ không hoạt động trong trình thực thi mô-đun. Việc chuyển đổi worker sang sử dụng các mô-đun JavaScript có nghĩa là tất cả mã đều được tải ở chế độ nghiêm ngặt. Một thay đổi đáng chú ý khác là giá trị của this ở phạm vi cấp cao nhất của mô-đun JavaScript là undefined, trong khi ở trình thực thi cổ điển, giá trị là phạm vi chung của trình thực thi. Rất may là luôn có một self toàn cục cung cấp tệp tham chiếu đến phạm vi toàn cục. API này có sẵn trong mọi loại worker, bao gồm cả worker dịch vụ, cũng như trong DOM.

Tải trước trình thực thi bằng modulepreload

Một điểm cải thiện đáng kể về hiệu suất đi kèm với trình thực thi mô-đun là khả năng tải trước trình thực thi và các phần phụ thuộc của trình thực thi. Với trình chạy mô-đun, tập lệnh được tải và thực thi dưới dạng mô-đun JavaScript chuẩn, nghĩa là các tập lệnh có thể được tải trước và thậm chí được phân tích cú pháp trước bằng modulepreload:

<!-- preloads worker.js and its dependencies: -->
<link rel="modulepreload" href="worker.js">

<script>
  addEventListener('load', () => {
    // our worker code is likely already parsed and ready to execute!
    const worker = new Worker('worker.js', { type: 'module' });
  });
</script>

Cả luồng chính và trình thực thi mô-đun cũng có thể sử dụng các mô-đun tải trước. Điều này rất hữu ích cho các mô-đun được nhập trong cả hai ngữ cảnh hoặc trong trường hợp không thể biết trước liệu một mô-đun sẽ được sử dụng trên luồng chính hay trong một worker.

Trước đây, các tuỳ chọn có sẵn để tải trước tập lệnh trình chạy web bị giới hạn và không nhất thiết phải đáng tin cậy. Worker cũ có loại tài nguyên "worker" riêng để tải trước, nhưng không có trình duyệt nào triển khai <link rel="preload" as="worker">. Do đó, kỹ thuật chính để tải trước trình thực thi web là sử dụng <link rel="prefetch">, vốn hoàn toàn dựa vào bộ nhớ đệm HTTP. Khi được sử dụng kết hợp với các tiêu đề lưu vào bộ nhớ đệm chính xác, tính năng này giúp bạn tránh được việc tạo thực thể của worker phải chờ tải tập lệnh worker xuống. Tuy nhiên, không giống như modulepreload, kỹ thuật này không hỗ trợ tải trước các phần phụ thuộc hoặc phân tích cú pháp trước.

Còn nhân viên dùng chung thì sao?

Kể từ Chrome 83, Worker dùng chung đã được cập nhật để hỗ trợ các mô-đun JavaScript. Giống như các trình thực thi chuyên dụng, việc tạo một trình thực thi dùng chung bằng tuỳ chọn {type:"module"} giờ đây sẽ tải tập lệnh trình thực thi dưới dạng một mô-đun thay vì một tập lệnh cổ điển:

const worker = new SharedWorker('/worker.js', {
  type: 'module'
});

Trước khi hỗ trợ các mô-đun JavaScript, hàm khởi tạo SharedWorker() chỉ dự kiến một URL và một đối số name không bắt buộc. Tính năng này sẽ tiếp tục hoạt động đối với cách sử dụng trình thực thi dùng chung kiểu cũ; tuy nhiên, việc tạo trình thực thi dùng chung mô-đun đòi hỏi phải sử dụng đối số options mới. Các tuỳ chọn có sẵn cũng giống như các tuỳ chọn dành cho một worker chuyên trách, bao gồm cả tuỳ chọn name thay thế đối số name trước đó.

Còn trình chạy dịch vụ thì sao?

Thông số kỹ thuật của trình chạy dịch vụ đã được cập nhật để hỗ trợ việc chấp nhận mô-đun JavaScript làm điểm truy cập, sử dụng cùng một tuỳ chọn {type:"module"} làm trình chạy mô-đun, tuy nhiên, thay đổi này vẫn chưa được triển khai trong trình duyệt. Khi điều đó xảy ra, bạn có thể tạo thực thể cho trình chạy dịch vụ bằng mô-đun JavaScript thông qua mã sau:

navigator.serviceWorker.register('/sw.js', {
  type: 'module'
});

Giờ đây, khi quy cách đã được cập nhật, các trình duyệt sẽ bắt đầu triển khai hành vi mới. Việc này cần thời gian vì có thêm một số chức năng liên quan đến việc đưa các mô-đun JavaScript vào trình chạy dịch vụ. Quy trình đăng ký trình chạy dịch vụ cần so sánh các tập lệnh đã nhập với các phiên bản đã lưu vào bộ nhớ đệm trước đó khi xác định xem có kích hoạt bản cập nhật hay không. Việc này cần được triển khai cho các mô-đun JavaScript khi dùng cho trình chạy dịch vụ. Ngoài ra, trình chạy dịch vụ cần có khả năng bỏ qua bộ nhớ đệm cho các tập lệnh trong một số trường hợp nhất định khi kiểm tra bản cập nhật.

Tài nguyên bổ sung và tài liệu đọc thêm