Giới thiệu bản dùng thử theo nguyên gốc Scheduler.Yield

Xây dựng trang web phản hồi nhanh 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 của hiệu suất web—một trong những khía cạnh mà Nhóm Chrome đã và đang nỗ lực làm việc để giúp các nhà phát triển web đáp ứng được. Chỉ trong năm nay, có thông báo rằng chỉ số Lượt 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ý. Công cụ này hiện đã sẵn sàng thay thế Thời gian phản hồi lần tương tác đầu tiên (FID) làm Chỉ số quan trọng chính của trang web vào tháng 3 năm 2024.

Với nỗ lực không ngừng cung cấp các API mới giúp nhà phát triển web làm cho trang web của họ hoạt động nhanh chóng nhất có thể, Nhóm Chrome hiện đang chạy 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 nội dung bổ sung mới được đề xuất cho API trình lập lịch biểu, giúp mang lại quyền kiểm soát cho luồng chính một cách dễ dàng và hiệu quả hơn so với các phương thức trước đây thường dựa vào.

Khi lợi nhuận

JavaScript sử dụng mô hình chạy để 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 chừng nào 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 được chuyển lại cho 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 trừ các trường hợp cực kỳ nghiêm trọng 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), hiệu suất là một khía cạnh tất yếu của logic lập lịch tác vụ của JavaScript. Điều đó sẽ xảy ra, chỉ là vấn đề thời điểm và sớm hơn sẽ tốt hơn. 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), thì 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 có tốc độ phản hồi kém, vì chúng làm chậm khả năng trình duyệt phản hồi hoạt động đầu vào của người dùng. Các tác vụ càng 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 đang chạy chậm hoặc thậm chí cảm thấy rằng trang đã bị hỏng.

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 cho đến khi tác vụ đó kết thúc trước khi quyền kiểm soát được đưa trở lại luồng chính. Bạn có thể cải thiện khả năng phản hồi hoạt động đầu vào của người dùng trên trang bằng cách tạo ra một tác vụ rõ ràng trong một tác vụ, giúp chia tác vụ cần hoàn thành ở cơ hội sẵn có tiếp theo. Nhờ đó, các tác vụ khác có thể 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.

Mô tả cách chia nhỏ một tác vụ có thể hỗ trợ khả năng phản hồi dữ liệu đầu vào tốt hơn. Ở trên cùng, một tác vụ dài sẽ chặn một trình xử lý sự kiện chạy cho đến khi tác vụ đó hoàn tất. Ở dưới cùng, tác vụ được chia nhỏ cho phép trình xử lý sự kiện chạy sớm hơn so với thường lệ.
Hình ảnh minh hoạ tính năng điều khiển quay lại luồng chính. Ở trên cùng, việc tạo ra chỉ xảy ra sau khi một tác vụ chạy đến khi hoàn thành, điều này có nghĩa là các tác vụ có thể mất nhiều thời gian để hoàn thành hơn trước khi trả lại quyền kiểm soát cho luồng chính. Ở dưới cùng, việc mang lại hiệu quả được thực hiện một cách rõ ràng, chia một tác vụ dài thành nhiều tác vụ nhỏ hơn. Điều này cho phép các tương tác của người dùng chạy sớm hơn, giúp cải thiện khả năng phản hồi đầu vào và INP.

Khi bạn thể hiện rõ ràng, bạn sẽ 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 một chút thời gian 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 do người dùng nhập hoặc các tác vụ khác có thể quan trọng". Đây là một công cụ giá trị trong bộ công cụ của nhà phát triển có thể giúp cải thiện trải nghiệm người dùng.

Vấn đề với các chiến lược lợi nhuận hiện tại

Một phương thức tạo ra phổ biến sử dụng setTimeout với giá trị thời gian chờ là 0. Cách này hiệu quả 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ì đợi trình duyệt tự tạo ra, bạn sẽ nói "hãy 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 mang lại bằng setTimeout có thể gây ra tác dụng phụ không mong muốn: tác vụ diễn ra sau điểm lợi nhuận sẽ quay lại cuối hàng đợi tác vụ. Những tác vụ do hoạt động tương tác của người dùng lên lịch sẽ vẫn xuất hiện trước hàng đợi như bình thường, nhưng công việc còn lại mà bạn muốn thực hiện sau khi tạo ra năng suất rõ ràng có thể bị các tác vụ khác của các nguồn cạnh tranh xếp hàng đợi trước đó trì hoãn thêm.

Để xem ví dụ thực tế, hãy dùng thử bản minh hoạ sự cố nhỏ này hoặc thử nghiệm bản minh hoạ này trong phiên bản 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 nhật ký khi các công việc được chạy. Khi bạn truy cập trang, hãy thực hiện các hành động sau:

  1. Nhấp vào nút trên cùng có nhãn Chạy tác vụ theo định kỳ. Thao tác này sẽ lên lịch các tác vụ chặn sẽ chạy thường xuyên. Khi bạn nhấp vào nút này, nhật ký tác vụ sẽ điền một vài thông báo Thao tác chặn tác vụ bằng setInterval.
  2. Tiếp theo, hãy nhấp vào nút có nhãn Run loop, tạo ra setTimeout trên mỗi vòng lặp.

Bạn sẽ 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

Kết quả này minh hoạ hành vi "kết thúc hàng đợi tác vụ" xảy ra khi tạo bằng setTimeout. Vòng lặp chạy xử lý 5 mục và tạo ra setTimeout sau khi mỗi mục được xử lý.

Hình này minh hoạ một vấn đề phổ biến trên web: việc 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 bộ tính giờ chạy công việc trong một khoảng thời gian nhất định là điều bình thường. Hành vi "kết thúc hàng đợi tác vụ" đi kèm với năng suất 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 tác vụ còn lại mà vòng lặp phải thực hiện sau khi tạo.

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. Tuy nhiên, trong nhiều trường hợp, đây là lý do khiến nhà phát triển cảm thấy ngần ngại từ bỏ quyền kiểm soát luồng chính một cách dễ dàng. Hiệu suất mang lại hiệu quả 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 cũng cho phép các hoạt động tương tác khác không phải do người dùng thực hiện để 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!

Đi vào scheduler.yield

scheduler.yield đã được cung cấp sau 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 bạn có thể có là "tại sao tôi cần một hàm đặc biệt để tạo ra khi setTimeout đã thực hiện điều đó?"

Cần lưu ý rằng việc tạo ra lợi nhuận không phải là mục tiêu thiết kế của setTimeout, mà là một tác dụng phụ tuyệt vời trong việc lên lịch chạy một lệnh gọi lại 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 tạo tác vụ bằng setTimeout sẽ gửi công việc còn lại đến quay lại 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 đầ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 tạo sẽ không ảnh hưởng đến các tác vụ từ các nguồn khác (ngoại trừ hoạt động tương tác đáng chú ý của người dùng).

scheduler.yield là một hàm mang lại luồng chính và trả về Promise khi được gọi. Tức 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 trong thực tế, hãy làm như sau:

  1. Chuyển đến chrome://flags.
  2. Bật thử nghiệm Các tính năng của Nền tảng web thử nghiệm. Bạn có thể phải khởi động lại Chrome sau khi thực hiện việc này.
  3. Chuyển đến trang minh hoạ hoặc sử dụng phiên bản được nhúng của trang đó trong danh sách này.
  4. Nhấp vào nút trên cùng có nhãn Chạy tác vụ định kỳ.
  5. Cuối cùng, hãy nhấp vào nút có nhãn Chạy vòng lặp, tạo ra scheduler.yield trên mỗi vòng 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ạ sử dụng setTimeout, bạn có thể thấy rằng vòng lặp (mặc dù nó mang lại sau mỗi lần lặp) không gửi công việc còn lại ra cuối hàng đợi mà gửi công việc còn lại lên phía trước hàng đợi. Điều này mang lại cho bạn cả hai tính năng tốt nhất: bạn có thể cải thiện khả năng phản hồi dữ liệu đầ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 hoạt động không bị trì hoãn.

Hãy dùng thử!

Nếu bạn thấy scheduler.yield thú vị và muốn dùng thử, thì bạn có thể thực hiện theo hai cách kể từ phiên bản 115 của Chrome:

  1. Nếu bạn muốn thử nghiệm với scheduler.yield cục bộ, hãy nhập và nhập chrome://flags vào thanh địa chỉ của Chrome rồi chọn Bật trên trình đơn thả xuống trong phần Các tính năng của nền tảng web thử nghiệm. Thao tác này sẽ chỉ cung cấp scheduler.yield (và mọi tính năng thử nghiệm khác) trong phiên bản Chrome của bạn.
  2. Nếu muốn bật scheduler.yield cho 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ốc scheduler.yield. Điều này cho phép bạn thử nghiệm một cách an toàn các tính năng được đề xuất trong khoảng thời gian nhất định và cung cấp cho Nhóm Chrome những 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 bản dùng thử theo nguyên gốc, hãy đọc hướng dẫn này.

Cách bạn dùng scheduler.yield (trong khi vẫn hỗ trợ các trình duyệt không triển khai) 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. Thẻ polyfill sẽ hữu ích nếu những điều sau đây áp dụng cho trường hợp của bạn:

  1. Bạn đã sử dụng scheduler.postTask trong ứng dụng để lên lịch tác vụ.
  2. Bạn muốn có khả năng thiết lập nhiệm vụ và mức độ ưu tiên.
  3. Bạn muốn huỷ hoặc sắp xếp lại mức độ ưu tiên của các nhiệm vụ thông qua lớp TaskController mà API scheduler.postTask cung cấp.

Nếu thông tin này không đúng với trường hợp của bạn, thì có thể polyfill này không phù hợp với bạn. Trong trường hợp đó, bạn có thể sử dụng tính năng 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ó sẵn, nhưng sẽ quay lại sử dụng 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, các trình duyệt không hỗ trợ scheduler.yield sẽ mang lại kết quả mà không có hành vi "trước hàng đợi". Nếu điều đó có nghĩa là bạn hoàn toàn không muốn mang lại, bạn có thể thử một phương pháp khác sử dụng scheduler.yield nếu có sẵn, nhưng sẽ hoàn toàn không mang lại kết quả 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 – hy vọng rằng các nhà phát triển hy vọng có thể 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 lợi nhuận hiện tại. Nếu scheduler.yield có vẻ là một API hữu ích đối với bạn, vui lòng tham gia nghiên cứu của chúng tôi để giúp cải thiện API đó và cung cấp ý kiến phản hồi về cách cải thiện API này.

Hình ảnh chính trên Unsplash, của Jonathan Allison.