Sử dụng scheduler.yield() để chia nhỏ các tác vụ dài

Ngày phát hành: 6 tháng 3 năm 2025

Browser Support

  • Chrome: 129.
  • Edge: 129.
  • Firefox: not supported.
  • Safari: not supported.

Source

Trang có cảm giác chậm và không phản hồi khi các tác vụ dài khiến luồng chính bị bận, ngăn luồng này thực hiện các công việc quan trọng khác, chẳng hạn như phản hồi hoạt động đầu vào của người dùng. Do đó, ngay cả các thành phần điều khiển biểu mẫu tích hợp sẵn cũng có thể bị người dùng cho là bị hỏng, như thể trang bị treo, chưa kể đến các thành phần tuỳ chỉnh phức tạp hơn.

scheduler.yield() là một cách để nhường cho luồng chính – cho phép trình duyệt chạy mọi công việc có mức độ ưu tiên cao đang chờ xử lý – sau đó tiếp tục thực thi từ nơi đã dừng. Điều này giúp trang phản hồi nhanh hơn, từ đó giúp cải thiện Hoạt động tương tác với lần vẽ tiếp theo (INP).

scheduler.yield cung cấp một API tiện lợi thực hiện đúng như nội dung mô tả: thực thi hàm được gọi trong các điểm tạm dừng tại biểu thức await scheduler.yield() và trả về luồng chính, chia nhỏ tác vụ. Quá trình thực thi phần còn lại của hàm (được gọi là phần tiếp tục của hàm) sẽ được lên lịch chạy trong một tác vụ vòng lặp sự kiện mới.

async function respondToUserClick() {
  giveImmediateFeedback();
  await scheduler.yield(); // Yield to the main thread.
  slowerComputation();
}

Lợi ích cụ thể của scheduler.yield là việc tiếp tục sau khi trả về được lên lịch chạy trước khi chạy bất kỳ tác vụ tương tự nào khác mà trang đã đưa vào hàng đợi. Phương thức này ưu tiên việc tiếp tục một tác vụ hơn là bắt đầu các tác vụ mới.

Bạn cũng có thể sử dụng các hàm như setTimeout hoặc scheduler.postTask để chia nhỏ các tác vụ, nhưng các phần tiếp tục đó thường chạy sau mọi tác vụ mới đã xếp hàng, có thể gây ra độ trễ lâu giữa việc chuyển sang luồng chính và hoàn tất công việc.

Ưu tiên tiếp tục sau khi nhường

scheduler.yield là một phần của API lên lịch công việc được ưu tiên. Là nhà phát triển web, chúng ta thường không nói về thứ tự mà vòng lặp sự kiện chạy các tác vụ theo mức độ ưu tiên rõ ràng, nhưng các mức độ ưu tiên tương đối luôn có sẵn, chẳng hạn như lệnh gọi lại requestIdleCallback chạy sau mọi lệnh gọi lại setTimeout đã xếp hàng hoặc trình nghe sự kiện đầu vào được kích hoạt thường chạy trước khi một tác vụ được xếp hàng bằng setTimeout(callback, 0).

Tính năng Lên lịch tác vụ theo mức độ ưu tiên giúp bạn dễ dàng xác định tác vụ nào sẽ chạy trước tác vụ khác và cho phép điều chỉnh mức độ ưu tiên để thay đổi thứ tự thực thi đó (nếu cần).

Như đã đề cập, việc tiếp tục thực thi một hàm sau khi trả về bằng scheduler.yield() sẽ có mức độ ưu tiên cao hơn so với việc bắt đầu các tác vụ khác. Ý tưởng chính là việc tiếp tục một tác vụ phải chạy trước, trước khi chuyển sang các tác vụ khác. Nếu tác vụ là mã hoạt động tốt, định kỳ trả về để trình duyệt có thể làm những việc quan trọng khác (chẳng hạn như phản hồi hoạt động đầu vào của người dùng), thì tác vụ đó không nên bị trừng phạt vì trả về bằng cách được ưu tiên sau các tác vụ tương tự khác.

Sau đây là ví dụ: hai hàm được đưa vào hàng đợi để chạy trong các tác vụ khác nhau bằng setTimeout.

setTimeout(myJob);
setTimeout(someoneElsesJob);

Trong trường hợp này, hai lệnh gọi setTimeout nằm ngay cạnh nhau, nhưng trong một trang thực tế, các lệnh gọi này có thể được gọi ở những vị trí hoàn toàn khác nhau, chẳng hạn như tập lệnh của bên thứ nhất và tập lệnh của bên thứ ba thiết lập công việc để chạy một cách độc lập, hoặc có thể là hai tác vụ từ các thành phần riêng biệt được kích hoạt sâu trong trình lập lịch biểu của khung.

Dưới đây là hình ảnh công việc đó trong DevTools:

Hai tác vụ hiển thị trong bảng điều khiển hiệu suất của Chrome DevTools. Cả hai đều được chỉ định là tác vụ dài, trong đó hàm "myJob" thực thi toàn bộ tác vụ đầu tiên và "someoneElsesJob" thực thi toàn bộ tác vụ thứ hai.

myJob được gắn cờ là một tác vụ dài, chặn trình duyệt thực hiện bất kỳ thao tác nào khác trong khi đang chạy. Giả sử đó là từ một tập lệnh của bên thứ nhất, chúng ta có thể chia nhỏ tập lệnh đó:

function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with setTimeout() to break up long task, then run part2.
  setTimeout(myJobPart2, 0);
}

myJobPart2 được lên lịch chạy với setTimeout trong myJob, nhưng lịch biểu đó chạy sau khi someoneElsesJob đã được lên lịch, nên quá trình thực thi sẽ diễn ra như sau:

Ba tác vụ hiển thị trong bảng điều khiển hiệu suất của Chrome DevTools. Đầu tiên là chạy hàm "myJobPart1", thứ hai là một tác vụ dài chạy "someoneElsesJob" và cuối cùng là tác vụ thứ ba chạy "myJobPart2".

Chúng ta đã chia nhỏ tác vụ bằng setTimeout để trình duyệt có thể phản hồi trong quá trình myJob, nhưng hiện tại, phần thứ hai của myJob chỉ chạy sau khi someoneElsesJob kết thúc.

Trong một số trường hợp, điều đó có thể ổn, nhưng thường thì không tối ưu. myJob đã nhường cho luồng chính để đảm bảo trang có thể tiếp tục phản hồi hoạt động đầu vào của người dùng, không phải từ bỏ hoàn toàn luồng chính. Trong trường hợp someoneElsesJob đặc biệt chậm hoặc nhiều công việc khác ngoài someoneElsesJob cũng đã được lên lịch, thì có thể phải mất một thời gian dài trước khi chạy nửa sau của myJob. Có thể đó không phải là ý định của nhà phát triển khi thêm setTimeout đó vào myJob.

Nhập scheduler.yield() để đưa phần tiếp tục của bất kỳ hàm nào gọi hàm đó vào hàng đợi có mức độ ưu tiên cao hơn một chút so với việc bắt đầu bất kỳ tác vụ tương tự nào khác. Nếu myJob được thay đổi để sử dụng:

async function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with scheduler.yield() to break up long task, then run part2.
  await scheduler.yield();
  myJobPart2();
}

Bây giờ, quá trình thực thi sẽ có dạng như sau:

Hai tác vụ hiển thị trong bảng điều khiển hiệu suất của Chrome DevTools. Cả hai đều được chỉ định là tác vụ dài, trong đó hàm "myJob" thực thi toàn bộ tác vụ đầu tiên và "someoneElsesJob" thực thi toàn bộ tác vụ thứ hai.

Trình duyệt vẫn có cơ hội phản hồi, nhưng giờ đây, việc tiếp tục tác vụ myJob được ưu tiên hơn so với việc bắt đầu tác vụ mới someoneElsesJob, vì vậy, myJob sẽ hoàn tất trước khi someoneElsesJob bắt đầu. Điều này gần với kỳ vọng nhường cho luồng chính để duy trì khả năng phản hồi, chứ không phải từ bỏ hoàn toàn luồng chính.

Kế thừa mức độ ưu tiên

Là một phần của API Lên lịch tác vụ được ưu tiên lớn hơn, scheduler.yield() kết hợp tốt với các mức độ ưu tiên rõ ràng có trong scheduler.postTask(). Nếu không đặt mức độ ưu tiên một cách rõ ràng, scheduler.yield() trong lệnh gọi lại scheduler.postTask() về cơ bản sẽ hoạt động giống như ví dụ trước.

Tuy nhiên, nếu bạn đặt mức độ ưu tiên, chẳng hạn như sử dụng mức độ ưu tiên 'background' thấp:

async function lowPriorityJob() {
  part1();
  await scheduler.yield();
  part2();
}

scheduler.postTask(lowPriorityJob, {priority: 'background'});

Phần tiếp tục sẽ được lên lịch với mức độ ưu tiên cao hơn các tác vụ 'background' khác – nhận phần tiếp tục được ưu tiên dự kiến trước mọi công việc 'background' đang chờ xử lý – nhưng vẫn có mức độ ưu tiên thấp hơn các tác vụ mặc định hoặc tác vụ có mức độ ưu tiên cao khác; đây vẫn là công việc 'background'.

Điều này có nghĩa là nếu bạn lên lịch công việc có mức độ ưu tiên thấp bằng 'background' scheduler.postTask() (hoặc bằng requestIdleCallback), thì việc tiếp tục sau scheduler.yield() bên trong cũng sẽ chờ cho đến khi hầu hết các tác vụ khác hoàn tất và luồng chính ở trạng thái rảnh để chạy. Đây chính xác là điều bạn muốn từ việc nhường quyền trong một công việc có mức độ ưu tiên thấp.

Cách sử dụng API

Hiện tại, scheduler.yield() chỉ có trong các trình duyệt dựa trên Chromium. Vì vậy, để sử dụng tính năng này, bạn cần phát hiện tính năng và quay lại phương thức nhường quyền thứ hai cho các trình duyệt khác.

scheduler-polyfill là một polyfill nhỏ cho scheduler.postTaskscheduler.yield, sử dụng kết hợp các phương thức nội bộ để mô phỏng nhiều tính năng của API lên lịch trong các trình duyệt khác (mặc dù không hỗ trợ tính năng kế thừa mức độ ưu tiên scheduler.yield()).

Đối với những người muốn tránh sử dụng polyfill, một phương pháp là trả về bằng cách sử dụng setTimeout() và chấp nhận việc mất một phần tiếp tục được ưu tiên, hoặc thậm chí không trả về trong các trình duyệt không được hỗ trợ nếu không chấp nhận được. Hãy xem tài liệu về scheduler.yield() trong phần Tối ưu hoá tác vụ dài để biết thêm thông tin.

Bạn cũng có thể sử dụng các loại wicg-task-scheduling để kiểm tra loại và hỗ trợ IDE nếu đang phát hiện tính năng scheduler.yield() và tự thêm phương thức dự phòng.

Tìm hiểu thêm

Để biết thêm thông tin về API và cách API tương tác với các mức độ ưu tiên của tác vụ và scheduler.postTask(), hãy xem tài liệu về scheduler.yield()Lên lịch tác vụ được ưu tiên trên MDN.

Để tìm hiểu thêm về các tác vụ dài, mức độ ảnh hưởng của các tác vụ này đến trải nghiệm người dùng và những việc cần làm, hãy đọc bài viết về cách tối ưu hoá các tác vụ dài.