Lập lịch JS hiệu quả hơn với isInputPending()

API JavaScript mới có thể giúp bạn tránh được sự đánh đổi giữa hiệu suất tải và khả năng phản hồi đầu vào.

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

Rất khó để tải nhanh. Các trang web tận dụng JS để hiển thị nội dung hiện phải đánh đổi giữa hiệu suất tải và khả năng phản hồi đầu vào: thực hiện tất cả công việc cần thiết để hiển thị cùng một lúc (hiệu suất tải tốt hơn, khả năng phản hồi đầu vào kém hơn) hoặc chia công việc thành các tác vụ nhỏ hơn để vẫn phản hồi được đầu vào và vẽ (hiệu suất tải kém hơn, khả năng phản hồi đầu vào tốt hơn).

Để không cần phải đánh đổi như vậy, Facebook đã đề xuất và triển khai API isInputPending() trong Chromium để cải thiện khả năng phản hồi mà không cần phải nhường quyền. Dựa trên ý kiến phản hồi về bản dùng thử theo nguyên gốc, chúng tôi đã cập nhật một số điểm cho API này và vui mừng thông báo rằng theo mặc định, API này hiện đang được vận chuyển trong Chromium 87!

Khả năng tương thích với trình duyệt

Hỗ trợ trình duyệt

  • Chrome: 87.
  • Edge: 87.
  • Firefox: không được hỗ trợ.
  • Safari: không được hỗ trợ.

Nguồn

isInputPending() được xuất bản trong các trình duyệt dựa trên Chromium kể từ phiên bản 87. Không có trình duyệt nào khác đã báo hiệu ý định gửi API.

Thông tin khái quát

Hầu hết công việc trong hệ sinh thái JS hiện nay đều được thực hiện trên một luồng duy nhất: luồng chính. Điều này cung cấp một mô hình thực thi mạnh mẽ cho nhà phát triển, nhưng trải nghiệm người dùng (đặc biệt là khả năng phản hồi) có thể bị ảnh hưởng nghiêm trọng nếu tập lệnh thực thi trong một thời gian dài. Ví dụ: nếu trang đang thực hiện nhiều việc trong khi sự kiện đầu vào được kích hoạt, ví dụ: trang sẽ không xử lý sự kiện nhập lượt nhấp cho đến khi công việc đó hoàn tất.

Phương pháp hay nhất hiện tại là giải quyết vấn đề này bằng cách chia JavaScript thành các khối nhỏ hơn. Trong khi tải, trang có thể chạy một chút JavaScript, sau đó trả về và chuyển quyền kiểm soát trở lại trình duyệt. Sau đó, trình duyệt có thể kiểm tra hàng đợi sự kiện đầu vào và xem liệu có điều gì cần thông báo cho trang hay không. Sau đó, trình duyệt có thể quay lại chạy các khối JavaScript khi các khối đó được thêm vào. Việc này có thể giúp ích nhưng cũng có thể gây ra các vấn đề khác.

Mỗi khi trang trả lại quyền kiểm soát cho trình duyệt, trình duyệt sẽ mất chút thời gian để kiểm tra hàng đợi sự kiện đầu vào, xử lý sự kiện và chọn khối JavaScript tiếp theo. Mặc dù trình duyệt phản hồi các sự kiện nhanh hơn, nhưng thời gian tải tổng thể của trang sẽ bị chậm lại. Và nếu chúng ta trả về quá thường xuyên, trang sẽ tải quá chậm. Nếu chúng ta mang lại ít thường xuyên hơn, trình duyệt sẽ mất nhiều thời gian hơn để phản hồi các sự kiện của người dùng và người dùng sẽ cảm thấy khó chịu. Không thú vị.

Sơ đồ cho thấy khi bạn chạy các tác vụ JS dài, trình duyệt sẽ có ít thời gian hơn để gửi sự kiện.

Tại Facebook, chúng tôi muốn xem mọi thứ sẽ như thế nào nếu chúng tôi đưa ra một phương pháp tải mới giúp loại bỏ sự đánh đổi khó chịu này. Chúng tôi đã liên hệ với các bạn bè của mình tại Chrome về vấn đề này và đưa ra đề xuất cho isInputPending(). API isInputPending() là API đầu tiên sử dụng khái niệm gián đoạn đối với dữ liệu đầu vào của người dùng trên web và cho phép JavaScript có thể kiểm tra dữ liệu đầu vào mà không cần chuyển sang trình duyệt.

Sơ đồ cho thấy isInputPending() cho phép JS kiểm tra xem có hoạt động đầu vào nào của người dùng đang chờ xử lý hay không mà không hoàn toàn tạo ra lệnh thực thi cho trình duyệt.

Vì có nhiều người quan tâm đến API này, nên chúng tôi đã hợp tác với các đồng nghiệp tại Chrome để triển khai và cung cấp tính năng này trong Chromium. Với sự trợ giúp của các kỹ sư Chrome, các bản vá đã được triển khai sau khi dùng thử theo nguyên gốc (đây là một cách để Chrome thử nghiệm các thay đổi và nhận ý kiến phản hồi của nhà phát triển trước khi phát hành đầy đủ API).

Chúng tôi hiện đã xem xét ý kiến phản hồi từ thử nghiệm gốc và từ các thành viên khác của Nhóm làm việc về hiệu suất web của W3C, đồng thời đã triển khai các thay đổi đối với API.

Ví dụ: trình lập lịch biểu mang lại lợi nhuận

Giả sử bạn có một loạt công việc chặn hiển thị cần làm để tải trang, chẳng hạn như tạo mã đánh dấu từ các thành phần, rút gọn các số nguyên tố hoặc chỉ vẽ một vòng quay tải thú vị. Mỗi một trong số này được chia thành một mục công việc riêng biệt. Sử dụng mẫu trình lập lịch biểu, hãy phác thảo cách chúng ta có thể xử lý công việc trong một hàm processWorkQueue() giả định:

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
 
if (performance.now() >= DEADLINE) {
   
// Yield the event loop if we're out of time.
    setTimeout
(processWorkQueue);
   
return;
 
}
  let job
= workQueue.shift();
  job
.execute();
}

Bằng cách gọi processWorkQueue() sau trong một macrotask mới thông qua setTimeout(), chúng ta cho phép trình duyệt có thể phản hồi một phần đối với dữ liệu đầu vào (trình duyệt có thể chạy trình xử lý sự kiện trước khi tiếp tục công việc) trong khi vẫn có thể chạy tương đối không bị gián đoạn. Tuy nhiên, chúng ta có thể bị tạm hoãn trong thời gian dài do công việc khác muốn kiểm soát vòng lặp sự kiện hoặc nhận thêm độ trễ sự kiện lên tới QUANTUM mili giây.

Đây là một cách làm ổn, nhưng chúng ta có thể làm tốt hơn không? Chắc chắn!

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
 
if (navigator.scheduling.isInputPending() || performance.now() >= DEADLINE) {
   
// Yield if we have to handle an input event, or we're out of time.
    setTimeout
(processWorkQueue);
   
return;
 
}
  let job
= workQueue.shift();
  job
.execute();
}

Bằng cách đưa ra lệnh gọi đến navigator.scheduling.isInputPending(), chúng ta có thể phản hồi đầu vào nhanh hơn mà vẫn đảm bảo rằng công việc chặn hiển thị của chúng ta thực thi liên tục. Nếu không muốn xử lý bất kỳ thao tác nào khác ngoài thao tác đầu vào (ví dụ: vẽ) cho đến khi công việc hoàn tất, chúng ta cũng có thể dễ dàng tăng chiều dài của QUANTUM.

Theo mặc định, các sự kiện "liên tục" sẽ không được trả về từ isInputPending(). Các thuộc tính này bao gồm mousemove, pointermove và các thuộc tính khác. Nếu bạn cũng muốn nhường cho các phương thức này, thì không vấn đề gì. Bằng cách cung cấp một đối tượng cho isInputPending() với includeContinuous được đặt thành true, chúng ta đã sẵn sàng:

const DEADLINE = performance.now() + QUANTUM;
const options = { includeContinuous: true };
while (workQueue.length > 0) {
 
if (navigator.scheduling.isInputPending(options) || performance.now() >= DEADLINE) {
   
// Yield if we have to handle an input event (any of them!), or we're out of time.
    setTimeout
(processWorkQueue);
   
return;
 
}
  let job
= workQueue.shift();
  job
.execute();
}

Vậy là xong! Các khung như React đang xây dựng tính năng hỗ trợ isInputPending() vào thư viện lên lịch cốt lõi của chúng bằng cách sử dụng logic tương tự. Hy vọng rằng điều này sẽ giúp các nhà phát triển sử dụng các khung này có thể hưởng lợi từ isInputPending() ở chế độ nền mà không cần phải viết lại đáng kể.

Lợi nhuận không phải lúc nào cũng xấu

Xin lưu ý rằng việc giảm số lượng kết quả trả về không phải là giải pháp phù hợp cho mọi trường hợp sử dụng. Có nhiều lý do để trả lại quyền kiểm soát cho trình duyệt ngoài việc xử lý các sự kiện đầu vào, chẳng hạn như để hiển thị và thực thi các tập lệnh khác trên trang.

Có những trường hợp trình duyệt không thể phân bổ đúng cách các sự kiện đầu vào đang chờ xử lý. Cụ thể, việc đặt các đoạn video và mặt nạ phức tạp cho các iframe trên nhiều nguồn gốc có thể báo cáo kết quả âm tính giả (tức là isInputPending() có thể đột ngột trả về giá trị false khi nhắm mục tiêu các khung này). Hãy đảm bảo rằng bạn thường xuyên trả về kết quả nếu trang web của bạn yêu cầu tương tác với các khung con được tạo kiểu.

Ngoài ra, hãy lưu ý đến các trang khác cũng chia sẻ một vòng lặp sự kiện. Trên các nền tảng như Chrome dành cho Android, việc nhiều nguồn gốc dùng chung một vòng lặp sự kiện là điều khá phổ biến. isInputPending() sẽ không bao giờ trả về true nếu dữ liệu đầu vào được gửi đến một khung đa nguồn gốc, do đó, các trang ở chế độ nền có thể can thiệp vào khả năng phản hồi của các trang ở chế độ nền trước. Bạn nên giảm, hoãn hoặc nhường quyền thường xuyên hơn khi làm việc ở chế độ nền bằng cách sử dụng API Khả năng hiển thị trang.

Bạn nên thận trọng khi sử dụng isInputPending(). Nếu không cần thực hiện tác vụ chặn người dùng nào, hãy đối xử tốt với những người khác trong vòng lặp sự kiện bằng cách nhường suất thường xuyên hơn. Các tác vụ dài có thể gây hại.

Phản hồi

  • Để lại ý kiến phản hồi về thông số kỹ thuật trong kho lưu trữ is-input-pending.
  • Liên hệ với @acomminos (một trong những tác giả của thông số kỹ thuật) trên Twitter.

Kết luận

Chúng tôi rất vui khi isInputPending() ra mắt và nhà phát triển có thể bắt đầu sử dụng ngay hôm nay. Đây là lần đầu tiên Facebook xây dựng một API web mới và đưa API đó từ giai đoạn ươm mầm ý tưởng đến đề xuất tiêu chuẩn để thực sự phân phối trong trình duyệt. Chúng tôi muốn cảm ơn tất cả những người đã giúp chúng tôi đến được thời điểm này, và đặc biệt cảm ơn tất cả mọi người tại Chrome đã giúp chúng tôi triển khai ý tưởng này và đưa ý tưởng đó đến với người dùng!

Ảnh chính do Will H McMahan chụp trên Unsplash.