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

Một 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 khi nhập.

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

Tải nhanh rất khó. 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: thực hiện tất cả công việc cần để 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 nhỏ công việc thành các nhiệm vụ nhỏ hơn để duy trì khả năng phản hồi với dữ liệu nhập và vẽ (hiệu suất tải kém hơn, phản hồi đầu vào tốt hơn).

Để loại bỏ nhu cầu đưa ra sự đánh đổi nà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 gây ra lỗi. 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 đối với API và vui mừng thông báo rằng API này hiện được vận chuyển 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

  • 87
  • 87
  • x
  • x

Đã phát hành isInputPending() trong trình duyệt dựa trên Chromium kể từ phiên bản 87. Chưa 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 ngày nay đều được thực hiện trên một luồng duy nhất: luồng chính. Điều này mang lại mô hình thực thi mạnh mẽ cho các nhà phát triển, nhưng trải nghiệm người dùng (cụ thể là khả năng phản hồi) có thể bị ảnh hưởng đáng kể nếu tập lệnh thực thi trong thời gian dài. Ví dụ: nếu trang đang thực hiện nhiều thao tá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 nhập lượt nhấp cho đến sau khi công việc đó hoàn tất.

Phương pháp hay nhất hiện tại là xử lý 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 đoạn 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 có thông tin gì cần cho trang biết hay không. Sau đó, trình duyệt có thể quay lại chạy các khối JavaScript khi chúng được thêm vào. Việc này có thể hữu ích, nhưng có thể gây ra các vấn đề khác.

Mỗi khi trang mang lại quyền kiểm soát cho trình duyệt, sẽ mất một khoảng thời gian để trình duyệt 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 tổng thời gian tải của trang lại bị chậm lại. Và nếu chúng tôi gặp phải tình trạng quá thường xuyên, trang sẽ tải quá chậm. Nếu chúng tôi gặp phải tình trạng ít thường xuyên hơn, thì 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ị chút nào.

Sơ đồ cho thấy khi bạn chạy các tác vụ JS trong thời gian 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 việc sẽ diễn ra như thế nào nếu chúng tôi nghĩ ra một phương pháp tải mới sẽ loại bỏ sự đánh đổi khó chịu này. Chúng tôi đã liên hệ với bạn bè tại Chrome về việc này và đưa ra đề xuất cho isInputPending(). API isInputPending() là API đầu tiên sử dụng khái niệm gây gián đoạn hoạt động đầu vào của người dùng trên web, đồng thời cho phép JavaScript có thể kiểm tra dữ liệu đầu vào mà không cần dẫn đến trình duyệt.

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

Vì quan tâm đến API, 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, chúng tôi đã cung cấp các bản vá sau một bản dùng thử theo nguyên gốc (đây là một cách để Chrome kiểm thử 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).

Hiện tại, chúng tôi đã tiếp thu ý kiến phản hồi từ bản dùng thử theo nguyên gốc và ý kiến của các thành viên khác trong Nhóm hoạt động về hiệu suất web của W3C, sau đó triển khai các thay đổi đối với API này.

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

Giả sử bạn đã thực hiện nhiều thao tác chặn hiển thị để tải trang của mình, chẳng hạn như tạo mã đánh dấu từ các thành phần, phân tích số nguyên tố hoặc chỉ vẽ một vòng quay tải thú vị. Mỗi một yếu tố trong số này được chia nhỏ thành một mục công việc riêng biệt. Bằng cách sử dụng mẫu trình lập lịch biểu, hãy phác thảo cách chúng tôi có thể xử lý công việc của mình trong 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 tác vụ macro mới thông qua setTimeout(), chúng tôi cho phép trình duyệt duy trì khả năng phản hồi một chút với hoạt động đầu vào (có thể chạy trình xử lý sự kiện trước khi công việc tiếp tụ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 tôi có thể bị trễ lịch trong một thời gian dài đối với những công việc khác (muốn kiểm soát vòng lặp sự kiện) hoặc độ trễ sự kiện lên đến QUANTUM mili giây.

Điều này có thể xảy ra, nhưng chúng ta có thể làm tốt hơn không? Chắc chắn rồi!

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 giới thiệu lệnh gọi đến navigator.scheduling.isInputPending(), chúng ta có thể phản hồi dữ liệu đầu vào nhanh hơn trong khi vẫn đảm bảo rằng công việc chặn hiển thị sẽ thực thi không bị gián đoạn. Nếu không muốn xử lý bất cứ điều 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ể dễ dàng tăng độ 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 tạo lợi nhuận cho những tính nă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 có thể:

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() cho các thư viện lập 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() phía sau các cảnh mà không cần viết lại đáng kể.

Hiệu suất không phải lúc nào cũng xấu

Lưu ý rằng việc tạo ít quảng cáo hơn 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ả về quyền kiểm soát cho trình duyệt ngoài lý do xử lý các sự kiện đầu vào, chẳng hạn như để thực hiện việc kết xuất và thực thi các tập lệnh khác trên trang.

Tồn tại các 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 thiết lập các đoạn video và mặt nạ phức tạp cho các iframe 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ể bất ngờ trả về giá trị false khi nhắm đến các khung này). Hãy đảm bảo thu nhập của bạn đủ thường xuyên nếu trang web của bạn yêu cầu tương tác với các khung phụ được cách điệu.

Bạn cũng nên lưu ý đến các trang khác có chung 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 sẽ dùng chung một vòng lặp sự kiệ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, trì hoãn hoặc lợi nhuận thường xuyên hơn khi thực hiện công việc ở chế độ nền bằng cách sử dụng Page Visibility API.

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

Ý kiến phản hồi

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

Kết luận

Chúng tôi rất vui mừng khi isInputPending() sắp ra mắt và các nhà phát triển có thể bắt đầu sử dụng nền tảng này ngay hôm nay. API này là lần đầu tiên Facebook xây dựng một API web mới và đưa API này từ bước ươm mầm ý tưởng đến đề xuất tiêu chuẩn để thực sự vận chuyển 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 mục tiêu này, đồng thời gửi lời cảm ơn đặc biệt đến tất cả mọi người tại Chrome đã giúp chúng tôi hiện thực hoá ý tưởng này và đưa ý tưởng đi vào hoạt động!

Ảnh chính của Will H McMahan trên Unsplash.