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

Việc xây dựng 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 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.

Hình ảnh mô tả cách việc chia nhỏ một tác vụ có thể hỗ trợ khả năng phản hồi đầu vào tốt hơn. Ở trên cùng, một tác vụ dài sẽ chặn 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 thành các phần cho phép trình xử lý sự kiện chạy sớm hơn.
Hình ảnh trực quan về việc trả lại quyền kiểm soát cho luồng chính. Ở trên cùng, việc nhường chỉ xảy ra sau khi một tác vụ chạy xong, tức là các tác vụ có thể mất nhiều thời gian hơn để hoàn tất trước khi trả lại quyền kiểm soát cho luồng chính. Ở dưới cùng, việc nhường quyền được thực hiện một cách rõ ràng, chia một tác vụ dài thành một số tác vụ nhỏ hơn. Điều này cho phép các lượt 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 nhường quyền một cách rõ ràng, bạn đang nói với trình duyệt rằng "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 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 đư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:

  1. 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ằng setInterval).
  2. 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ằng setTimeout trên mỗi lần lặp lại).

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:

  1. Chuyển đến chrome://flags.
  2. 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.
  3. 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.
  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 Run loop, yielding with scheduler.yield on each iteration (Chạy vòng lặp, trả về bằng scheduler.yield trên mỗi lần lặp lại).

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 115 của Chrome:

  1. Nếu bạn muốn thử nghiệm với scheduler.yield trên máy, hãy nhập chrome://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ấ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 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ốc scheduler.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:

  1. Bạn đang sử dụng scheduler.postTask trong ứng dụng để lên lịch công việc.
  2. Bạn muốn có thể đặt mức độ ưu tiên cho tác vụ và việc trả về.
  3. 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à API scheduler.postTask cung cấp.

Nếu tình huống của bạn không giống như mô tả ở trên, thì có thể bạn không cần dùng 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ể 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ẽ 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.