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.
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ố nội dung cho API và rất vui mừng được thông báo rằng API hiện đang được phân phối theo mặc định trong Chromium 87!
Khả năng tương thích với trình duyệt
isInputPending()
được phân phối 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 công việc trong khi một sự kiện đầu vào được kích hoạt, thì trang sẽ không xử lý sự kiện đầu vào nhấp chuột 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 ít nhường 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à mọi người sẽ cảm thấy khó chịu. Không vui.
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.
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. Nhờ sự trợ giúp của các kỹ sư Chrome, chúng tôi đã phát hành các bản vá trong một bản dùng thử theo nguyên gốc (đây là cách Chrome kiểm thử các thay đổi và nhận ý kiến phản hồi từ nhà phát triển trước khi phát hành đầy đủ một 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ụ: một trình lập lịch biểu có khả năng trả về
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 quản lý để chạy tương đối
không bị gián đoạn. Tuy nhiên, chúng ta có thể bị huỷ lịch trong một 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 có thêm độ trễ sự kiện lên đến QUANTUM
mili giây.
Điều này cũng ổ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 API này bao gồm mousemove
, pointermove
và các API 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ể.
Việc trả về không phải lúc nào cũng là điều 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ể trả về giá trị sai một cách không mong muốn khi nhắm đến 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 cho Android, việc nhiều nguồn gốc chia sẻ một vòng lặp sự kiện là 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 sử dụng isInputPending()
một cách thận trọng. Nếu không có công việc chặn người dùng nào cần làm, 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 trả về 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.