Giờ đây, việc di chuyển các tác vụ nặng vào luồng ở chế độ nền trở nên dễ dàng hơn nhờ các mô-đun JavaScript trong trình chạy web.
JavaScript là đơn luồng, nghĩa là mỗi lúc chỉ có thể thực hiện một thao tác. Cách này rất trực quan và hoạt động hiệu quả trong nhiều trường hợp trên web, nhưng có thể gặp vấn đề khi chúng ta cần thực hiện các tác vụ nặng 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 có nhiều ứng dụng phức tạp được phân phối trên web, nhu cầu xử lý nhiều luồng cũng tăng lên.
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 lớp trừu tượng nhẹ trên luồng hệ điều hành hiển thị một API truyền thông báo để giao tiếp giữa các 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 hoạt động trên các tập dữ liệu lớn, cho phép luồng chính chạy trơn tru 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 ở chế độ 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');
}
});
API Web Worker đã có trong hầu hết các trình duyệt trong hơn 10 năm. Mặc dù điều đó có nghĩa là worker có khả năng hỗ trợ trình duyệt tuyệt vời và được tối ưu hoá hiệu quả, nhưng cũng có nghĩa là worker xuất hiện trước các mô-đun JavaScript. 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 cổ điển
Hàm khởi tạo Worker lấy URL tập lệnh cổ điển, tương ứng với URL tài liệu. Phương thức này sẽ trả về ngay một tệp tham chiếu đến thực thể worker mới, hiển thị giao diện nhắn tin cũng như phương thức terminate()
để ngay lập tức dừng và huỷ 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. Thẻ 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, nghĩa là các biến trong một tập lệnh có thể bị ghi đè bởi các biến trong một tập lệnh khác.
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ình chạy web trước đây đã tác động quá lớn đến cấu trúc của ứng dụng. Nhà phát triển đã phải tạo ra các công cụ và giải pháp thông minh để có thể sử dụng worker web mà không từ bỏ các phương pháp phát triển hiện đại. Ví dụ: các trình đóng 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 gói các mô-đun trong các hàm để tránh xung đột biến và mô phỏng việc nhập và xuất phần phụ thuộc.
Nhập worker mô-đun
Một chế độ mới cho worker web với các lợi ích về hiệu suất và công thái học của mô-đun JavaScript sẽ được cung cấp trong Chrome 80, được gọi là worker mô-đun. Hàm khởi tạo Worker
hiện chấp nhận tuỳ chọn {type:"module"}
mới, tuỳ chọn này sẽ 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ì worker mô-đun là các mô-đun JavaScript 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, worker, v.v.) và tất cả các lần nhập sau này đều tham chiếu đến thực thể mô-đun đã thực thi. Trình duyệt cũng tối ưu hoá việc tải và thực thi các mô-đun JavaScript. Bạn có thể tải các phần phụ thuộc của mô-đun trước khi mô-đun được thực thi, cho phép tải song song 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 sử dụng trên luồng chính và trong một worker chỉ cần được phân tích cú pháp một lần.
Việc chuyển sang mô-đun JavaScript cũng cho phép sử dụng tính năng nhập động để tải mã 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 rõ ràng hơn nhiều so với việc sử dụng importScripts()
để tải các phần phụ thuộc, vì các phần xuất của mô-đun đã nhập đượ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ũ không có trong worker 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
trong phạm vi cấp cao nhất của mô-đun JavaScript là undefined
, trong khi ở worker cổ điển, giá trị này là phạm vi toàn cục của worker. May mắn thay, luôn có một self
toàn cục cung cấp tham chiếu đến phạm vi toàn cục. Phương thức này có sẵn trong tất cả các loại worker, bao gồm cả worker dịch vụ, cũng như trong DOM.
Tải trước worker bằng modulepreload
Một điểm cải thiện hiệu suất đáng kể đi kèm với worker mô-đun là khả năng tải trước worker và các phần phụ thuộc của worker. Với worker mô-đun, các tập lệnh được tải và thực thi dưới dạng mô-đun JavaScript tiêu chuẩn, tức là các tập lệnh này 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 tải trước tập lệnh trình chạy web bị hạn chế và không nhất thiết phải đáng tin cậy. Worker kiểu 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, điều này giúp bạn có thể tránh việc tạo thực thể 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 worker 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ư worker chuyên dụng, việc tạo worker dùng chung bằng tuỳ chọn {type:"module"}
hiện sẽ tải tập lệnh worker dưới dạng mô-đun thay vì 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. Phương thức này sẽ tiếp tục hoạt động đối với cách sử dụng worker dùng chung kiểu cũ; tuy nhiên, việc tạo worker dùng chung mô-đun yêu cầu sử dụng đối số options
mới. Các tuỳ chọn có sẵn giống với các tuỳ chọn cho worker chuyên dụng, 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 các trình duyệt. Sau khi điều đó xảy ra, bạn có thể tạo bản sao của worker dịch vụ bằng cách sử dụng mô-đun JavaScript bằng 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. Quá trình này sẽ mất thời gian vì có một số vấn đề phức tạp liên quan đến việc đưa các mô-đun JavaScript vào worker 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
- Trạng thái tính năng, sự đồng thuận và chuẩn hoá của trình duyệt
- Thêm thông số kỹ thuật của worker mô-đun gốc
- Mô-đun JavaScript dành cho worker dùng chung
- Mô-đun JavaScript dành cho trình chạy dịch vụ: Trạng thái triển khai Chrome