Việc tạo các trang web phản hồi nhanh với dữ liệu đầu vào của người dùng là một trong những khía cạnh khó khăn nhất về hiệu suất web – một khía cạnh mà Nhóm Chrome đã nỗ lực giúp các nhà phát triển web đáp ứng. Chỉ trong năm nay, đã có thông báo rằng chỉ số Lượt tương tác đến nội dung hiển thị tiếp theo (INP) sẽ chuyển từ trạng thái thử nghiệm sang trạng thái đang chờ xử lý. Giờ đây, INP sắp thay thế Độ trễ đầu vào đầu tiên (FID) làm một Chỉ số quan trọng chính của trang web vào tháng 3 năm 2024.
Trong nỗ lực không ngừng cung cấp các API mới giúp nhà phát triển web tạo ra những trang web nhanh nhất có thể, Nhóm Chrome hiện đang chạy một bản dùng thử theo nguyên gốc cho scheduler.yield bắt đầu từ phiên bản 115 của Chrome. scheduler.yield là một đề xuất bổ sung mới cho API trình lập lịch, cho phép cả cách thức dễ dàng và hiệu quả hơn để trả lại quyền kiểm soát cho luồng chính so với các phương thức thường được sử dụng.
Khi nhường quyền
JavaScript sử dụng mô hình chạy đến khi hoàn thành để xử lý các tác vụ. Điều này có nghĩa là khi một tác vụ chạy trên luồng chính, tác vụ đó sẽ chạy trong thời gian cần thiết để hoàn tất. Sau khi một tác vụ hoàn tất, quyền kiểm soát sẽ được chuyển trở lại luồng chính, cho phép luồng chính xử lý tác vụ tiếp theo trong hàng đợi.
Ngoài những trường hợp cực đoan khi một tác vụ không bao giờ hoàn thành (ví dụ: vòng lặp vô hạn), việc tạo ra là một khía cạnh không thể tránh khỏi trong logic lập lịch tác vụ của JavaScript. Điều đó sẽ xảy ra, chỉ là vấn đề khi nào, và càng sớm càng tốt. Khi các tác vụ mất quá nhiều thời gian để chạy (chính xác là hơn 50 mili giây), chúng được coi là tác vụ dài.
Tác vụ có thời gian thực hiện dài là nguyên nhân khiến trang có khả năng phản hồi kém, vì chúng làm chậm khả năng phản hồi hoạt động đầu vào của người dùng của trình duyệt. Tác vụ càng dài và càng thường xuyên xảy ra thì người dùng càng có nhiều khả năng cảm thấy trang hoạt động chậm chạp, hoặc thậm chí cảm thấy trang hoàn toàn bị lỗi.
Tuy nhiên, chỉ vì mã của bạn bắt đầu một tác vụ trong trình duyệt không có nghĩa là bạn phải đợi cho đến khi tác vụ đó hoàn tất thì quyền kiểm soát mới được trả về luồng chính. Bạn có thể cải thiện khả năng phản hồi đối với dữ liệu đầu vào của người dùng trên một trang bằng cách tạo một cách rõ ràng trong một tác vụ. Thao tác này sẽ chia tác vụ thành nhiều phần để hoàn thành vào thời điểm có thể thực hiện tiếp theo. Điều này cho phép các tác vụ khác có thời gian trên luồng chính sớm hơn so với việc chúng phải đợi các tác vụ dài hoàn tất.
Khi bạn nhường quyền một cách rõ ràng, bạn đang nói với trình duyệt "này, tôi hiểu rằng công việc mà tôi sắp làm có thể mất một lúc và tôi không muốn bạn phải thực hiện tất cả công việc đó trước khi phản hồi thông tin đầu vào của người dùng hoặc các tác vụ khác cũng có thể quan trọng". Đây là một công cụ có giá trị trong bộ công cụ của nhà phát triển, có thể giúp cải thiện đáng kể trải nghiệm người dùng.
Vấn đề với các chiến lược mang lại lợi nhuận hiện tại
Một phương thức phổ biến để tạo là sử dụng setTimeout với giá trị thời gian chờ là 0. Điều này có hiệu quả vì lệnh gọi lại được truyền đến setTimeout sẽ chuyển phần còn lại của công việc sang một tác vụ riêng biệt sẽ được đưa vào hàng đợi để thực thi sau đó. Thay vì đợi trình duyệt tự tạo ra, bạn đang nói "chia phần công việc lớn này thành các phần nhỏ hơn".
Tuy nhiên, việc tạo ra với setTimeout có thể gây ra một tác dụng phụ không mong muốn: công việc diễn ra sau điểm tạo ra sẽ chuyển đến cuối hàng đợi tác vụ. Các tác vụ do người dùng tương tác sẽ vẫn được đưa lên đầu hàng đợi như bình thường, nhưng những việc còn lại mà bạn muốn làm sau khi nhường quyền một cách rõ ràng có thể bị trì hoãn thêm do các tác vụ khác từ các nguồn cạnh tranh được xếp hàng đợi trước đó.
Để xem hiệu ứng này hoạt động, hãy dùng thử bản minh hoạ này trên Codepen hoặc thử nghiệm với hiệu ứng này trong phiên bản được nhúng sau đây. Bản minh hoạ này bao gồm một số nút mà bạn có thể nhấp vào và một hộp bên dưới các nút đó để ghi lại thời điểm các tác vụ được chạy. Khi bạn truy cập vào trang này, hãy thực hiện các thao tác sau:
- Nhấp vào nút trên cùng có nhãn Chạy các tác vụ định kỳ. Nút này sẽ lên lịch chạy các tác vụ chặn theo định kỳ. Khi bạn nhấp vào nút này, nhật ký tác vụ sẽ điền sẵn một số thông báo có nội dung Ran blocking task with
setInterval(Đã chạy tác vụ chặn bằngsetInterval). - Tiếp theo, hãy nhấp vào nút có nhãn Run loop, yielding with
setTimeouton each iteration (Chạy vòng lặp, tạo rasetTimeouttrên mỗi lần lặp lại).
Bạn sẽ nhận thấy hộp ở cuối bản minh hoạ sẽ có nội dung như sau:
Processing loop item 1
Processing loop item 2
Ran blocking task via setInterval
Processing loop item 3
Ran blocking task via setInterval
Processing loop item 4
Ran blocking task via setInterval
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Đầu ra này minh hoạ hành vi "kết thúc hàng đợi tác vụ" xảy ra khi tạo ra với setTimeout. Vòng lặp chạy các quy trình gồm 5 mục và tạo ra setTimeout sau khi mỗi mục được xử lý.
Điều này minh hoạ một vấn đề thường gặp trên web: không có gì lạ khi một tập lệnh (đặc biệt là tập lệnh của bên thứ ba) đăng ký một hàm hẹn giờ chạy công việc theo một khoảng thời gian nào đó. Hành vi "kết thúc hàng đợi tác vụ" đi kèm với việc tạo ra bằng setTimeout có nghĩa là công việc từ các nguồn tác vụ khác có thể được xếp hàng đợi trước công việc còn lại mà vòng lặp phải thực hiện sau khi tạo ra.
Tuỳ thuộc vào ứng dụng của bạn, đây có thể là kết quả mong muốn hoặc không mong muốn. Tuy nhiên, trong nhiều trường hợp, hành vi này là lý do khiến nhà phát triển có thể cảm thấy không muốn từ bỏ quyền kiểm soát luồng chính một cách dễ dàng. Việc tạo ra là điều tốt vì các lượt tương tác của người dùng có cơ hội chạy sớm hơn, nhưng việc này cũng cho phép các hoạt động khác không phải là lượt tương tác của người dùng có thời gian trên luồng chính. Đây là một vấn đề thực sự, nhưng scheduler.yield có thể giúp bạn giải quyết vấn đề này!
Nhập scheduler.yield
scheduler.yield đã có sẵn sau một cờ dưới dạng tính năng nền tảng web thử nghiệm kể từ phiên bản 115 của Chrome. Một câu hỏi mà bạn có thể thắc mắc là "tại sao tôi cần một hàm đặc biệt để tạo khi setTimeout đã làm việc đó?"
Điều đáng chú ý là việc tạo ra không phải là mục tiêu thiết kế của setTimeout, mà là một tác dụng phụ hữu ích khi lên lịch cho một lệnh gọi lại để chạy vào thời điểm sau này trong tương lai – ngay cả khi bạn chỉ định giá trị thời gian chờ là 0. Tuy nhiên, điều quan trọng hơn cần nhớ là việc tạo bằng setTimeout sẽ gửi phần việc còn lại đến cuối hàng đợi tác vụ. Theo mặc định, scheduler.yield sẽ gửi phần việc còn lại đến đầu hàng đợi. Điều này có nghĩa là công việc bạn muốn tiếp tục ngay sau khi nhường quyền sẽ không bị các tác vụ từ những nguồn khác (ngoại trừ các lượt tương tác của người dùng) chiếm quyền.
scheduler.yield là một hàm tạo ra luồng chính và trả về Promise khi được gọi. Điều này có nghĩa là bạn có thể await nó trong một hàm async:
async function yieldy () {
// Do some work...
// ...
// Yield!
await scheduler.yield();
// Do some more work...
// ...
}
Để xem scheduler.yield hoạt động, hãy làm như sau:
- Chuyển đến
chrome://flags. - Bật thử nghiệm Các tính năng thử nghiệm của nền tảng web. Bạn có thể phải khởi động lại Chrome sau khi thực hiện việc này.
- Chuyển đến trang minh hoạ hoặc sử dụng phiên bản được nhúng sau đây của trang minh hoạ này sau danh sách này.
- Nhấp vào nút trên cùng có nhãn Chạy các tác vụ định kỳ.
- Cuối cùng, hãy nhấp vào nút có nhãn Run loop, yielding with
scheduler.yieldon each iteration (Chạy vòng lặp, tạo rascheduler.yieldtrên mỗi lần lặp lại).
Kết quả đầu ra trong hộp ở cuối trang sẽ có dạng như sau:
Processing loop item 1
Processing loop item 2
Processing loop item 3
Processing loop item 4
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Không giống như bản minh hoạ tạo ra bằng cách sử dụng setTimeout, bạn có thể thấy rằng vòng lặp (mặc dù tạo ra sau mỗi lần lặp) không gửi phần công việc còn lại vào cuối hàng đợi mà là vào đầu hàng đợi. Điều này mang lại cho bạn cả hai lợi ích: bạn có thể nhường quyền kiểm soát để cải thiện khả năng phản hồi đầu vào trên trang web của mình, nhưng cũng đảm bảo rằng công việc bạn muốn hoàn thành sau khi nhường quyền kiểm soát không bị chậm trễ.
Hãy dùng thử!
Nếu thấy scheduler.yield thú vị và muốn dùng thử, bạn có thể làm theo 2 cách sau đây kể từ phiên bản 115 của Chrome:
- Nếu bạn muốn thử nghiệm
scheduler.yieldcục bộ, hãy nhập và nhấn Enterchrome://flagsvào thanh địa chỉ của Chrome rồi chọn Bật trong trình đơn thả xuống ở mục Các tính năng thử nghiệm của nền tảng web. Thao tác này sẽ chỉ cung cấpscheduler.yield(và mọi tính năng thử nghiệm khác) trong phiên bản Chrome của bạn. - Nếu muốn bật
scheduler.yieldcho người dùng Chromium thực trên một nguồn có thể truy cập công khai, bạn cần đăng ký bản dùng thử theo nguyên gốcscheduler.yield. Điều này cho phép bạn thử nghiệm an toàn các tính năng được đề xuất trong một khoảng thời gian nhất định và cung cấp cho Nhóm Chrome thông tin chi tiết có giá trị về cách các tính năng đó được sử dụng trên thực tế. Để biết thêm thông tin về cách hoạt động của các thử nghiệm nguồn gốc, hãy đọc hướng dẫn này.
Cách bạn sử dụng scheduler.yield (trong khi vẫn hỗ trợ những trình duyệt không triển khai scheduler.yield) sẽ tuỳ thuộc vào mục tiêu của bạn. Bạn có thể sử dụng polyfill chính thức. Polyfill sẽ hữu ích nếu bạn gặp phải trường hợp sau:
- Bạn đang sử dụng
scheduler.postTasktrong ứng dụng của mình để lên lịch cho các tác vụ. - Bạn muốn có thể đặt mức độ ưu tiên cho tác vụ và mức độ ưu tiên cho việc tạo ra.
- Bạn muốn có thể huỷ hoặc sắp xếp lại thứ tự ưu tiên của các tác vụ bằng cách sử dụng lớp
TaskControllermà APIscheduler.postTaskcung cấp.
Nếu không phải trường hợp của bạn, thì có thể polyfill không phù hợp với bạn. Trong trường hợp đó, bạn có thể tự triển khai phương án dự phòng theo một số cách. Phương pháp đầu tiên sử dụng scheduler.yield nếu có, nhưng sẽ quay lại setTimeout nếu không có:
// A function for shimming scheduler.yield and setTimeout:
function yieldToMain () {
// Use scheduler.yield if it exists:
if ('scheduler' in window && 'yield' in scheduler) {
return scheduler.yield();
}
// Fall back to setTimeout:
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
// Example usage:
async function doWork () {
// Do some work:
// ...
await yieldToMain();
// Do some other work:
// ...
}
Cách này có thể hoạt động, nhưng như bạn có thể đoán, những trình duyệt không hỗ trợ scheduler.yield sẽ tạo ra hành vi không phải là "đầu hàng đợi". Nếu điều đó có nghĩa là bạn không muốn tạo ra bất kỳ sự khác biệt nào, bạn có thể thử một phương pháp khác sử dụng scheduler.yield nếu có, nhưng sẽ không tạo ra bất kỳ sự khác biệt nào nếu không có:
// A function for shimming scheduler.yield with no fallback:
function yieldToMain () {
// Use scheduler.yield if it exists:
if ('scheduler' in window && 'yield' in scheduler) {
return scheduler.yield();
}
// Fall back to nothing:
return;
}
// Example usage:
async function doWork () {
// Do some work:
// ...
await yieldToMain();
// Do some other work:
// ...
}
scheduler.yield là một điểm bổ sung thú vị cho API trình lập lịch biểu. Hy vọng rằng điểm bổ sung này sẽ giúp nhà phát triển cải thiện khả năng phản hồi dễ dàng hơn so với các chiến lược tạo khoảng trống hiện tại. Nếu bạn thấy scheduler.yield là một API hữu ích, vui lòng tham gia nghiên cứu của chúng tôi để giúp cải thiện API này và gửi ý kiến phản hồi về cách cải thiện API này hơn nữa.
Hình ảnh chính trên Unsplash, của Jonathan Allison.