Giờ đây, việc chuyển các thao tác nặng vào các luồng trong nền trở nên dễ dàng hơn nhờ các mô-đun JavaScript trong trình chạy dịch vụ 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. Điều 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ây ra 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 về quy trình xử lý đa luồng cũng tăng lên.
Trên nền tảng web, nguyên tắc cơ bản chính để tạo luồng và tính song song là Web Workers API. Worker là một lớp trừu tượng đơn giản nằm trên các luồng của 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ể vô cùng 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.
Sau đây là một ví dụ điển hình về cách sử dụng worker, trong đó một tập lệnh worker lắng nghe tin nhắn từ luồng chính và phản hồi bằng cách gửi lại tin nhắn của riêng mình:
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ó mặt 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à các worker có khả năng 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 đã có từ lâu trước các mô-đun JavaScript. Vì không có hệ thống mô-đun nào khi các worker được thiết kế, nên API để tải mã vào một worker và tạo 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 vào năm 2009.
Nhật ký: nhân viên cổ điển
Hàm khởi tạo Worker lấy URL classic script (tập lệnh cổ điển), tương ứng với URL tài liệu. Thao tác này sẽ trả về ngay một tham chiếu đến phiên bản trình thực thi mới, phiên bản này sẽ hiển thị một giao diện nhắn tin cũng như một phương thức terminate()
giúp dừng và huỷ trình thực thi ngay lập tức.
const worker = new Worker('worker.js');
Hàm importScripts()
có sẵn trong các worker trên web để tải thêm mã, nhưng hàm này sẽ tạm dừng việc 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ị các biến trong một 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, các worker trên web từ trước đến nay đã có ảnh hưởng quá lớn đến cấu 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 thay thế thông minh để có thể sử dụng web worker 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 cách triển khai trình tải mô-đun nhỏ vào mã đã tạo sử dụng importScripts()
để tải mã, nhưng bao bọc các mô-đun trong các hàm để tránh xung đột biến và mô phỏng các lệnh nhập và xuất phần phụ thuộc.
Nhập worker mô-đun
Một chế độ mới cho các worker trên web với lợi ích về hiệu suất và công thái học của các mô-đun JavaScript đang được phát hành trong Chrome 80, được gọi là worker mô-đun. Hàm khởi tạo Worker
hiện chấp nhận một lựa chọn {type:"module"}
mới, giúp 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 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, worker, v.v.) và tất cả các lần nhập trong tương lai đều tham chiếu đến phiên bản mô-đun đã thực thi. Các trình duyệt cũng tối ưu hoá quá trình tải và thực thi các mô-đun JavaScript. Các phần phụ thuộc của mô-đun có thể được tải trước khi mô-đun được thực thi, cho phép toàn bộ cây mô-đun được tải song song. Quá trình tải mô-đun cũng lưu vào bộ nhớ đệm mã đã phân tích cú pháp, tức là các mô-đun được 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 các mô-đun JavaScript cũng cho phép sử dụng dynamic import để tải mã theo cách trì hoã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 thành phần xuất của mô-đun đã nhập sẽ được trả về thay vì dựa vào các biến chung.
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 các worker mô-đun. Việc chuyển đổi các worker để sử dụng mô-đun JavaScript có nghĩa là tất cả mã sẽ đượ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ột mô-đun JavaScript là undefined
, trong khi ở các worker cổ điển, giá trị này là phạm vi chung của worker. Rất may, luôn có một self
trên toàn cầu cung cấp thông tin tham chiếu đến phạm vi toàn cầu. API này có trong tất cả các loại worker, kể cả service worker, 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ể khi sử dụng worker mô-đun là khả năng tải trước các worker và phần phụ thuộc của chúng. Với các worker mô-đun, tập lệnh được tải và thực thi dưới dạng các mô-đun JavaScript tiêu chuẩn, tức là chúng có thể được tải trước và thậm chí được phân tích cú pháp trước bằng cách sử dụ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à các worker mô-đun đều có thể dùng các mô-đun được tải sẵn. Điều này hữu ích cho các mô-đun được nhập trong cả hai bối cảnh hoặc trong trường hợp không thể biết trước liệu một mô-đun sẽ được dùng trên luồng chính hay trong một worker.
Trước đây, các lựa chọn có sẵn để tải trước tập lệnh web worker bị hạn chế và không nhất thiết phải đáng tin cậy. Các worker cổ điển 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 có sẵn để tải trước các worker trên web là sử dụng <link rel="prefetch">
, hoàn toàn dựa vào bộ nhớ đệm HTTP. Khi được dùng kết hợp với tiêu đề lưu vào bộ nhớ đệm chính xác, điều này giúp tránh trường hợp quá trình khởi tạo trình chạy dịch vụ phải chờ tải tập lệnh của trình chạy dịch vụ 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 về các worker dùng chung thì sao?
Shared worker đã được cập nhật để hỗ trợ các mô-đun JavaScript kể từ Chrome 83. Giống như các worker chuyên dụng, việc tạo một worker dùng chung bằng lựa chọn {type:"module"}
hiện tải tập lệnh worker 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ỉ mong đợi một URL và một đối số name
không bắt buộc. Điều này sẽ tiếp tục hoạt động đối với việc sử dụng worker dùng chung cổ điển; 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 lựa chọn có sẵn giống như các lựa chọn dành cho một worker chuyên dụng, bao gồm cả lựa 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ột mô-đun JavaScript làm điểm truy cập, sử dụng cùng một lựa chọn {type:"module"}
như 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ể khởi tạo một worker dịch vụ bằng mô-đun JavaScript bằng đoạn mã sau:
navigator.serviceWorker.register('/sw.js', {
type: 'module'
});
Giờ đây, sau khi thông số kỹ thuật được cập nhật, các trình duyệt đang bắt đầu triển khai hành vi mới. Việc này mất thời gian vì có một số vấn đề phức tạp khác liên quan đến việc đưa các mô-đun JavaScript vào worker dịch vụ. Quá trình đăng ký worker dịch vụ cần so sánh các tập lệnh đã nhập với phiên bản được lưu vào bộ nhớ đệm trước đó khi xác định xem có nên kích hoạt bản cập nhật hay không. Bạn cần triển khai việc này cho các mô-đun JavaScript khi dùng cho worker dịch vụ. Ngoài ra, các worker 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.