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

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

JavaScript là đơn luồng, tức là chỉ có thể thực hiện một thao tác tại một thời điểm. Đây là cách trực quan và phù hợp trong nhiều trường hợp trên web, nhưng có thể trở thành vấn đề khi chúng ta cần thực hiện các công việc nặng nhọc 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, bạn càng cần phải xử lý đa luồng.

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à API Trình chạy web. Worker là một yếu tố trừu tượng nhẹ ở phía trên các 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 thao tác trên các tập dữ liệu lớn, cho phép luồng chính vận hành 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 đó một tập lệnh trình chạy 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ó trên hầu hết các trình duyệt từ hơn 10 năm nay. Mặc dù điều đó có nghĩa là worker có dịch vụ hỗ trợ trình duyệt rất tốt và được tối ưu hoá tốt, nhưng điều đó cũng có nghĩa là worker đã có từ lâu các mô-đun JavaScript. Vì không có hệ thống mô-đun khi thiết kế trình thực thi, API để tải mã vào trình thực thi 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 vào năm 2009.

Nhật ký: worker cũ

Hàm khởi tạo Worker sẽ lấy URL tập lệnh kiểu cũ liên quan đến URL của tài liệu. Trình thực thi này ngay lập tức trả về một tệp tham chiếu đến thực thể trình thực thi mới, qua đó hiển thị giao diện thông báo cũng như phương thức terminate() để 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 trình thực thi trên web để tải mã bổ sung, nhưng hàm này sẽ tạm dừng quá trình thực thi của trình thực thi đó để tìm nạp và đánh giá từng tập lệnh. Mã 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ể được ghi đè bằng 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ước đây trình thực thi web đã tạo ra một hiệu ứng quá lớn đối với 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 trình thực thi 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 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 vào các hàm để tránh xung đột biến đổi và mô phỏng quá trình 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 trình chạy web với các lợi ích về hiệu suất và tính hiệu quả của mô-đun JavaScript đang được đưa vào 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 việc 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 thực thi mô-đun là các mô-đun JavaScript tiêu chuẩn, nên trình thực thi này có thể sử dụng câu lệnh nhập và xuất. Giống như tất 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ần nhập trong tương lai đều tham chiếu đến thực thể 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á. Bạn có thể tải các phần phụ thuộc của một mô-đun trước khi thực thi mô-đun, nhờ đó, toàn bộ các cây mô-đun được tải song song. Việc tải mô-đun cũng sẽ lưu mã đã phân tích cú pháp vào bộ nhớ đệm, nghĩa là các mô-đun được sử dụng trên luồng chính và trong một trình thực thi 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 cho 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 việc 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 đượ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 tốt, phương thức importScripts() cũ không có trong trình thực thi mô-đun. Việc chuyển worker để sử dụng 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 ở các trình thực thi cổ điển, giá trị này là phạm vi chung của trình thực thi. May mắn là luôn có một tập lệnh toàn cục self tham chiếu đến phạm vi toàn cục. Nó có sẵn trong mọi loại worker bao gồm dịch vụ worker cũng như trong DOM.

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

Một điểm cải tiế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 này. Với trình thực thi mô-đun, tập lệnh sẽ được tải và thực thi dưới dạng mô-đun JavaScript tiêu chuẩn, có nghĩa là bạn có thể 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 đều có thể sử dụng các mô-đun tải sẵn. Điều này 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 dùng trên luồng chính hay trong một trình thực thi.

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 đá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 có sẵn để tải trước trình thực thi web là sử dụng <link rel="prefetch">, 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 đề bộ nhớ đệm chính xác, điều này giúp bạn có thể tránh việc tạo bản sao của trình thực thi phải đợi để tải tập lệnh trình thực thi xuống. Tuy nhiên, không giống như modulepreload, kỹ thuật này không hỗ trợ việc tải trước các phần phụ thuộc hoặc phân tích cú pháp trước.

Còn trình thực thi dùng chung thì sao?

Kể từ Chrome 83, trình chạy dùng chung đã được cập nhật để hỗ trợ các mô-đun JavaScript. Giống như 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"} hiện 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. Thao tác này sẽ tiếp tục hoạt động khi 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 yêu cầu sử dụng đối số options mới. Các tuỳ chọn có sẵn sẽ giống như các tuỳ chọn dành cho một trình thực thi 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 thực thi mô-đun, tuy nhiên thay đổi này vẫn chưa được triển khai trong các 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 khi dùng mã sau:

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

Hiện tại, quy cách đã được cập nhật, nên 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ố chức năng khác liên quan đến việc đưa các mô-đun JavaScript vào trình chạy dịch vụ. Việc đă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. Đồng thời, 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 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 liệu tham khảo bổ sung và đọc thêm