Sử dụng web worker để chạy JavaScript ngoài luồng chính của trình duyệt

Kiến trúc ngoài luồng chính có thể cải thiện đáng kể độ tin cậy và trải nghiệm người dùng của ứng dụng.

Trong 20 năm qua, web đã phát triển đáng kể từ các tài liệu tĩnh với một vài kiểu và hình ảnh thành các ứng dụng động và phức tạp. Tuy nhiên, có một điều hầu như không thay đổi: chúng tôi chỉ có một luồng trên mỗi thẻ trình duyệt (trừ một số ngoại lệ) để thực hiện công việc hiển thị trang web và chạy JavaScript.

Do đó, luồng chính đã bị quá tải. Khi các ứng dụng web ngày càng phức tạp, luồng chính sẽ trở thành nút thắt cổ chai đáng kể đối với hiệu suất. Thậm chí còn tệ hơn là khoảng thời gian cần thiết để chạy mã trên luồng chính đối với một người dùng nhất định gần như hoàn toàn không thể đoán trước vì các tính năng của thiết bị ảnh hưởng rất lớn đến hiệu suất. Tình trạng khó dự đoán này sẽ ngày càng gia tăng khi người dùng truy cập web từ ngày càng đa dạng các thiết bị, từ điện thoại tính năng siêu hạn chế đến các máy mạnh mẽ, có tốc độ làm mới cao.

Nếu muốn các ứng dụng web tinh vi đáp ứng một cách thoả đáng các nguyên tắc về hiệu suất như Chỉ số quan trọng chính của trang web (dựa trên dữ liệu thực nghiệm về nhận thức và tâm lý của con người) thì chúng tôi cần có cách thực thi mã ngoài luồng chính (OMT).

Tại sao nên sử dụng nhân viên web?

Theo mặc định, JavaScript là ngôn ngữ đơn luồng chạy các tác vụ trên luồng chính. Tuy nhiên, trình chạy web cung cấp một lối thoát khỏi luồng chính bằng cách cho phép nhà phát triển tạo các luồng riêng để xử lý công việc ngoài luồng chính. Mặc dù phạm vi của nhân viên web bị hạn chế và không cung cấp quyền truy cập trực tiếp vào DOM, nhưng họ có thể cực kỳ hưởng lợi nếu cần phải thực hiện lượng công việc đáng kể có thể làm quá tải luồng chính.

Trong trường hợp có liên quan đến Chỉ số quan trọng chính của trang web, việc chạy tác vụ ngoài luồng chính có thể mang lại lợi ích. Cụ thể, việc giảm tải tác vụ từ luồng chính đến trình thực thi web có thể giảm tranh chấp đối với luồng chính, nhờ đó cải thiện chỉ số về khả năng phản hồi Tương tác với nội dung hiển thị tiếp theo (INP) của trang. Khi luồng chính có ít thao tác để xử lý hơn, luồng đó có thể phản hồi nhanh hơn với các tương tác của người dùng.

Ít hoạt động của luồng chính hơn (đặc biệt là trong quá trình khởi động) cũng có thể mang lại lợi ích tiềm năng cho Nội dung lớn nhất hiển thị (LCP) nhờ giảm các tác vụ mất nhiều thời gian. Việc hiển thị phần tử LCP đòi hỏi thời gian của luồng chính – để hiển thị văn bản hoặc hình ảnh (vốn là phần tử LCP thường xuyên và thường xuyên) – và bằng cách giảm tổng công việc của luồng chính, bạn có thể đảm bảo rằng phần tử LCP của trang ít bị chặn bởi tác vụ tốn kém mà một nhân viên web có thể xử lý thay thế.

Tạo luồng bằng worker web

Các nền tảng khác thường hỗ trợ công việc song song bằng cách cho phép bạn cung cấp cho một luồng một hàm chạy song song với phần còn lại của chương trình. Bạn có thể truy cập vào các biến giống nhau từ cả hai luồng và có thể đồng bộ hoá quyền truy cập vào các tài nguyên dùng chung này với mutex và semaphores để ngăn chặn tình huống tương tranh.

Trong JavaScript, chúng ta có thể nhận được chức năng gần như tương tự từ trình chạy web, đã có từ năm 2007 và được hỗ trợ trên tất cả các trình duyệt chính kể từ năm 2012. Trình chạy web chạy song song với luồng chính, nhưng không giống như luồng hệ điều hành, trình chạy này không thể chia sẻ các biến.

Để tạo một trình chạy web, hãy truyền một tệp vào hàm khởi tạo worker để bắt đầu chạy tệp đó trong một luồng riêng:

const worker = new Worker("./worker.js");

Giao tiếp với nhân viên web bằng cách gửi thông báo qua API postMessage. Truyền giá trị thông báo dưới dạng tham số trong lệnh gọi postMessage, sau đó thêm trình nghe sự kiện thông báo vào worker:

main.js

const worker = new Worker('./worker.js');
worker.postMessage([40, 2]);

worker.js

addEventListener('message', event => {
  const [a, b] = event.data;

  // Do stuff with the message
  // ...
});

Để gửi thông báo trở lại luồng chính, hãy sử dụng cùng một API postMessage trong web worker và thiết lập trình nghe sự kiện trên luồng chính:

main.js

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

worker.postMessage([40, 2]);
worker.addEventListener('message', event => {
  console.log(event.data);
});

worker.js

addEventListener('message', event => {
  const [a, b] = event.data;

  // Do stuff with the message
  postMessage(a + b);
});

Phải thừa nhận rằng phương pháp này có phần hạn chế. Trước đây, nhân viên web chủ yếu được dùng để di chuyển một phần công việc nặng nhọc ra khỏi luồng chính. Việc cố gắng xử lý nhiều thao tác với một nhân viên web sẽ trở nên khó sử dụng một cách nhanh chóng: bạn phải mã hoá không chỉ các thông số mà còn phải mã hoá hoạt động trong thông báo và bạn phải thực hiện công việc ghi sổ kế toán để so khớp các phản hồi với yêu cầu. Sự phức tạp đó có thể là lý do khiến nhân viên web chưa được áp dụng rộng rãi hơn.

Nhưng nếu chúng ta có thể loại bỏ một số khó khăn khi giao tiếp giữa luồng chính và trình thực thi web, thì mô hình này có thể phù hợp cho nhiều trường hợp sử dụng. Và thật may là có một thư viện chỉ giúp bạn làm việc đó!

Comlink là một thư viện có mục tiêu là cho phép bạn sử dụng nhân viên web mà không cần phải suy nghĩ về các chi tiết của postMessage. Comlink cho phép bạn chia sẻ các biến giữa trình thực thi web và luồng chính gần như giống như các ngôn ngữ lập trình khác hỗ trợ phân luồng.

Bạn thiết lập Comlink bằng cách nhập Comlink vào web worker và xác định tập hợp hàm để hiển thị luồng chính. Sau đó, bạn nhập Comlink trên luồng chính, gói worker và truy cập vào các hàm đã hiển thị:

worker.js

import {expose} from 'comlink';

const api = {
  someMethod() {
    // ...
  }
}

expose(api);

main.js

import {wrap} from 'comlink';

const worker = new Worker('./worker.js');
const api = wrap(worker);

Biến api trên luồng chính hoạt động giống như biến trong trình thực thi web, ngoại trừ việc mỗi hàm trả về một hứa hẹn cho một giá trị thay vì chính giá trị đó.

Bạn nên chuyển mã nào sang một nhân viên web?

Nhân viên web không có quyền truy cập vào DOM và nhiều API như WebUSB, WebRTC hoặc Âm thanh web, vì vậy, bạn không thể đưa các phần của ứng dụng dựa vào quyền truy cập như vậy vào một worker. Tuy nhiên, mọi đoạn mã nhỏ được chuyển đến một worker đều mua thêm khoảng trống trên luồng chính cho những nội dung phải có ở đó, chẳng hạn như cập nhật giao diện người dùng.

Một vấn đề đối với các nhà phát triển web là hầu hết các ứng dụng web đều dựa vào khung giao diện người dùng như Vue hoặc React để sắp xếp mọi thứ trong ứng dụng; mọi thứ đều là một thành phần của khung và do đó vốn được gắn liền với DOM. Việc này có vẻ gây khó khăn cho việc di chuyển sang kiến trúc OMT.

Tuy nhiên, nếu chúng ta chuyển sang một mô hình mà trong đó mối quan tâm về giao diện người dùng được tách biệt với các vấn đề khác, chẳng hạn như quản lý trạng thái, thì trình thực thi web có thể khá hữu ích ngay cả với các ứng dụng dựa trên khung. Đó chính xác là phương pháp mà PROXX thực hiện.

PROXX: nghiên cứu điển hình về OMT

Nhóm Google Chrome đã phát triển PROXX dưới dạng một bản sao của Minesweeper, đáp ứng các yêu cầu của Ứng dụng web tiến bộ, bao gồm cả hoạt động ngoại tuyến và mang đến trải nghiệm hấp dẫn cho người dùng. Thật không may, các phiên bản ban đầu của trò chơi hoạt động kém hiệu quả trên những thiết bị hạn chế như điện thoại phổ thông. Điều này khiến nhóm nhận ra rằng luồng chính là điểm tắc nghẽn.

Nhóm đã quyết định sử dụng nhân viên web để tách trạng thái hình ảnh của trò chơi khỏi logic của nó:

  • Luồng chính xử lý việc kết xuất ảnh động và hiệu ứng chuyển đổi.
  • Một nhân viên web xử lý logic trò chơi, vốn chỉ thuần tuý hoạt động tính toán.

OMT có những tác động thú vị đối với hiệu suất của điện thoại phổ thông của PROXX. Trong phiên bản không phải OMT, giao diện người dùng bị treo trong 6 giây sau khi người dùng tương tác với giao diện. Không có phản hồi nào và người dùng phải đợi đủ 6 giây thì mới có thể làm việc khác.

Thời gian phản hồi của giao diện người dùng trong phiên bản không phải của OMT của PROXX.

Tuy nhiên, trong phiên bản OMT, trò chơi cần 12 giây để hoàn tất quá trình cập nhật giao diện người dùng. Mặc dù điều này có vẻ làm giảm hiệu suất, nhưng thực tế lại giúp tăng lượng phản hồi cho người dùng. Tình trạng chậm trễ xảy ra do ứng dụng đang vận chuyển nhiều khung hình hơn so với phiên bản không phải của OMT, tức là phiên bản này không vận chuyển bất kỳ khung hình nào. Do đó, người dùng biết rằng có điều gì đó đang xảy ra và có thể tiếp tục chơi khi giao diện người dùng cập nhật, làm cho trò chơi có cảm giác tốt hơn đáng kể.

Thời gian phản hồi của giao diện người dùng trong phiên bản OMT của PROXX.

Đây là một sự đánh đổi có chủ ý: chúng tôi mang đến cho người dùng những thiết bị bị hạn chế trải nghiệm cảm thấy tốt hơn mà không phạt người dùng thiết bị cao cấp.

Ngụ ý của cấu trúc OMT

Như trong ví dụ PROXX, OMT giúp ứng dụng của bạn chạy ổn định trên nhiều loại thiết bị hơn, nhưng không giúp ứng dụng của bạn nhanh hơn:

  • Bạn chỉ di chuyển công việc khỏi luồng chính chứ không giảm bớt công việc.
  • Mức hao tổn giao tiếp tăng thêm giữa nhân viên web và luồng chính đôi khi có thể làm cho mọi thứ chậm hơn một chút.

Cân nhắc các ưu điểm, khuyết điểm

Vì luồng chính miễn phí để xử lý các hoạt động tương tác của người dùng như cuộn trong khi JavaScript đang chạy, nên có ít khung hình bị rớt hơn mặc dù tổng thời gian chờ có thể lâu hơn một chút. Để người dùng chờ một chút thì họ nên bỏ khung hình vì biên độ sai số sẽ nhỏ hơn đối với những khung hình bị rớt: tình trạng bỏ khung hình xảy ra tính bằng mili giây, trong khi bạn có hàng trăm mili giây trước khi người dùng thấy được thời gian chờ.

Do không thể dự đoán hiệu suất trên các thiết bị, mục tiêu của kiến trúc OMT thực sự là về giảm rủi ro — giúp ứng dụng của bạn hoạt động hiệu quả hơn trước những điều kiện thời gian chạy có thể thay đổi nhiều — không phải về lợi ích về hiệu suất của việc song song hoá. Sự gia tăng khả năng thích ứng và những cải tiến về trải nghiệm người dùng sẽ đem lại nhiều lợi ích hơn cho sự đánh đổi nhỏ về tốc độ.

Lưu ý về công cụ

Nhân viên web chưa phải là công cụ chính nên hầu hết các công cụ mô-đun (như webpackTổng hợp) không hỗ trợ công cụ này ngay từ đầu. (Tuy nhiên, Parcel vẫn áp dụng!) May mắn thay, có các trình bổ trợ giúp nhân viên web hoạt động với webpack và Rollup:

Tổng hợp

Để đảm bảo ứng dụng hoạt động ổn định và dễ tiếp cận nhất có thể, đặc biệt là trong một thị trường ngày càng toàn cầu hoá, chúng tôi cần hỗ trợ các thiết bị bị hạn chế. Đó là cách mà hầu hết người dùng truy cập web trên toàn cầu. OMT đưa ra một cách đầy hứa hẹn để tăng hiệu suất trên các thiết bị như vậy mà không ảnh hưởng xấu đến người dùng thiết bị cao cấp.

Ngoài ra, OMT còn có những lợi ích phụ sau:

  • Công cụ này di chuyển chi phí thực thi JavaScript sang một luồng riêng.
  • Nó di chuyển chi phí phân tích cú pháp, nghĩa là giao diện người dùng có thể khởi động nhanh hơn. Điều đó có thể làm giảm tỷ lệ Hiển thị nội dung đầu tiên hay thậm chí là Thời gian tương tác, Điều này có thể giúp tăng Điểm số Lighthouse.

Nhân viên web không nhất thiết phải sợ hãi. Các công cụ như Comlink đang loại bỏ công việc của worker và khiến chúng trở thành một lựa chọn khả thi cho hàng loạt ứng dụng web.

Hình ảnh chính trong bộ phim Unsplash của James Peacock.