Việc xây dựng các trang web phản hồi nhanh với hoạt động đầ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. Đây cũng là vấn đề mà Nhóm Chrome đang nỗ lực giải quyết để giúp các nhà phát triển web đáp ứng. Chỉ trong năm nay, chúng tôi đã công bố rằng chỉ số Hoạt động tương tác với 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ý. Chỉ số này hiện đang chuẩn bị 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 nhằm cung cấp các API mới giúp nhà phát triển web tạo ra các trang web nhanh nhất có thể, Nhóm Chrome hiện đang chạy một bản dùng thử gốc cho scheduler.yield
bắt đầu từ phiên bản 115 của Chrome. scheduler.yield
là một phần bổ sung mới được đề xuất cho API trình lập lịch biểu, cho phép cả cách dễ dàng và tốt 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 truyền thống đã dựa vào.
Khi nhường
JavaScript sử dụng mô hình chạy đến khi hoàn tất để 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. 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 các trường hợp cực đoan khi một tác vụ không bao giờ kết thúc (chẳng hạn như vòng lặp vô hạn), việc trả về là một khía cạnh không thể tránh khỏi trong logic lên lịch tác vụ của JavaScript. Việc này sẽ xảy ra, chỉ là vấn đề thời điểm và tốt hơn hết là nên làm 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.
Các tác vụ dài là nguyên nhân khiến trang phản hồi kém, vì chúng làm chậm khả năng của trình duyệt trong việc phản hồi hoạt động đầu vào của người dùng. Càng thường xuyên xảy ra các tác vụ dài và càng chạy lâu, thì càng có nhiều khả năng người dùng có ấn tượng rằng trang bị chậm hoặc thậm chí cảm thấy trang bị hỏng hoàn toàn.
Tuy nhiên, việc mã của bạn khởi động một tác vụ trong trình duyệt không có nghĩa là bạn phải đợi đến khi tác vụ đó hoàn tất thì mới trả lại quyền kiểm soát cho luồng chính. Bạn có thể cải thiện khả năng phản hồi đối với hoạt động đầu vào của người dùng trên một trang bằng cách trả về một cách rõ ràng trong một tác vụ, giúp chia nhỏ tác vụ để hoàn thành vào cơ hội 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 khi phải chờ các tác vụ dài kết thúc.
Khi nhường quyền một cách rõ ràng, bạn đang nói với trình duyệt rằng "này, tôi hiểu rằng công việc tôi sắp làm có thể mất chút thời gian và tôi không muốn bạn phải làm tất cả công việc đó trước khi phản hồi dữ liệu đầ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 nhường quyền hiện tại
Một phương pháp phổ biến để tạo ra sử dụng setTimeout
với giá trị thời gian chờ là 0
. Điều này hoạt động vì lệnh gọi lại được truyền đến setTimeout
sẽ di chuyển công việc còn lại sang một tác vụ riêng biệt sẽ được đưa vào hàng đợi để thực thi tiếp theo. Thay vì chờ trình duyệt tự trả về, bạn sẽ nói "hãy chia khối công việc lớn này thành các phần nhỏ hơn".
Tuy nhiên, việc trả về bằng setTimeout
có thể gây ra một hiệu ứng phụ không mong muốn: công việc diễn ra sau điểm trả về sẽ chuyển về cuối hàng đợi tác vụ. Các tác vụ được lên lịch theo lượt tương tác của người dùng vẫn sẽ được đưa vào đầu hàng đợi như bình thường. Tuy nhiên, công việc còn lại mà bạn muốn làm sau khi nhường quyền 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 đưa vào hàng đợi trước đó.
Để xem cách hoạt động của tính năng này, hãy thử bản minh hoạ Glitch này hoặc thử nghiệm trong phiên bản được nhúng bên dưới. Bản minh hoạ bao gồm một vài 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 chạy tác vụ. 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 Run tasks periodically (Chạy tác vụ định kỳ). Thao tác 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
setTimeout
on each iteration (Chạy vòng lặp, trả về bằngsetTimeout
trên mỗi lần lặp).
Bạn sẽ thấy hộp ở cuối bản minh hoạ 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
Kết quả này minh hoạ hành vi "kết thúc hàng đợi tác vụ" xảy ra khi trả về bằng setTimeout
. Vòng lặp chạy xử lý 5 mục và trả về bằng 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 trên một số khoảng thời gian. Hành vi "kết thúc hàng đợi tác vụ" đi kèm với việc trả về bằng setTimeout
có nghĩa là công việc từ các nguồn tác vụ khác có thể được đưa vào 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 trả về.
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ể miễn cưỡng từ bỏ quyền kiểm soát luồng chính. Việc nhường quyền là tốt vì các hoạt động tương tác của người dùng có cơ hội chạy sớm hơn, nhưng cũng cho phép các hoạt động tương tác không phải của người dùng khác 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 giải quyết vấn đề này!
Nhập scheduler.yield
scheduler.yield
đã có sẵn dưới dạng một tính năng nền tảng web thử nghiệm kể từ phiên bản 115 của Chrome. Bạn có thể thắc mắc "tại sao tôi cần một hàm đặc biệt để trả về khi setTimeout
đã thực hiện việc này?"
Xin lưu ý rằng việc trả về không phải là mục tiêu thiết kế của setTimeout
, mà là một hiệu ứng phụ tốt trong việc lên lịch gọi lại để chạy vào một thời điểm sau này trong tương lai – ngay cả khi đã 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 trả về bằng setTimeout
sẽ gửi công việc còn lại đến phần sau của hàng đợi tác vụ. Theo mặc định, scheduler.yield
sẽ gửi công việc còn lại đến phần đầu của 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 sẽ không bị các tác vụ từ các nguồn khác lấn át (ngoại trừ các lượt tương tác của người dùng).
scheduler.yield
là một hàm trả về 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
trong 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 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 của trang đó bên dưới danh sách này.
- Nhấp vào nút trên cùng có nhãn Chạy 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.yield
on each iteration (Chạy vòng lặp, trả về bằngscheduler.yield
trên mỗi lần lặp).
Kết quả 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 kết quả 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 kết quả sau mỗi lần lặp lại) không gửi 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 những lợi ích tốt nhất của cả hai phương pháp: bạn có thể nhường để cải thiện khả năng phản hồi đầu vào trên trang web, 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 không bị trì hoãn.
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 hai cách kể từ phiên bản Chrome 115:
- Nếu bạn muốn thử nghiệm với
scheduler.yield
trên máy, hãy nhậpchrome://flags
vào thanh địa chỉ của Chrome rồi chọn Bật trong trình đơn thả xuống trong phần 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.yield
cho người dùng Chromium thực trên một nguồn gốc 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, đồng thời cung cấp cho Nhóm Chrome thông tin chi tiết có giá trị về cách sử dụng các tính năng đó trong thực tế. Để biết thêm thông tin về cách hoạt động của thử nghiệm theo 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ợ các trình duyệt không triển khai scheduler.yield
) phụ thuộc vào mục tiêu của bạn. Bạn có thể sử dụng polyfill chính thức. Mã polyfill sẽ hữu ích nếu trường hợp của bạn thuộc một trong những trường hợp sau:
- Bạn đang sử dụng
scheduler.postTask
trong ứng dụng để lên lịch công việc. - Bạn muốn có thể đặt mức độ ưu tiên cho tác vụ và việc trả về.
- Bạn muốn có thể huỷ hoặc sắp xếp lại thứ tự ưu tiên cho các việc cần làm thông qua lớp
TaskController
mà APIscheduler.postTask
cung cấp.
Nếu tình huống của bạn không giống như vậy, thì có thể bạn không cần đến polyfill. Trong trường hợp đó, bạn có thể triển khai phương thức dự phòng của riêng mình 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ể hiệu quả, nhưng như bạn có thể đoán, những trình duyệt không hỗ trợ scheduler.yield
sẽ không có hành vi "đầu hàng đợi". Nếu điều đó có nghĩa là bạn không muốn nhường quyền thực thi, 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 nhường quyền thực thi 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 phần bổ sung thú vị cho API trình lập lịch biểu. API này hy vọng sẽ giúp nhà phát triển dễ dàng cải thiện khả năng phản hồi hơn so với các chiến lược trả về 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à cung cấp ý 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.