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 cân bằng giữa hiệu suất tải và khả năng phản hồi đầu vào: cùng lúc thực hiện tất cả các tác vụ cần thiết để hiển thị (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 nhỏ công việc đó thành các tác vụ nhỏ hơn để duy trì khả năng phản hồi với hoạt động đầ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

Hỗ trợ trình duyệt

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

Nguồn

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. Khi trang đang tải, trang có thể chạy một bit JavaScript, sau đó tạo 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 trình duyệt có cần thông báo gì 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 này đượ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 trao lại quyền kiểm soát cho trình duyệt, trình duyệt sẽ mất 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. Nếu chúng ta tạo trang 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 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 cần hoàn toàn trả về quá trì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. 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 đã tiếp nhận ý kiến phản hồi từ bản dùng thử theo nguyên gốc và từ các thành viên khác của Nhóm công tác về hiệu suất web W3C, đồng thời triển khai các thay đổi đối với API này.

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 có thể 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.

Đâ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 cứ thứ gì khác ngoài dữ liệu đầu vào (ví dụ: vẽ) cho đến khi công việc hoàn tất, chúng ta cũng có thể tăng độ dài của QUANTUM một cách thủ công.

Theo mặc định, các sự kiện "liên tục" 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 tạo điều kiện cho những người dùng này thì cũng không sao cả. 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 mà 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 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 trên nhiều nguồn gốc. Do đó, các trang chạy ở chế độ nền có thể ảnh hưởng đến khả năng phản hồi của các trang trên 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. Những 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 đạt được điểm này và gửi lời cảm ơn đặc biệt đến tất cả mọi người tại Chrome, những người đã giúp chúng tôi củng cố ý tưởng này và vận chuyển ý tưởng!

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