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

Việc tạo 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 thách thức nhất về hiệu suất web—một trong những khía cạnh mà Nhóm Chrome đã và đang nỗ lực hết mình để giúp các nhà phát triển web đáp ứng. Ngay trong năm nay, chúng tôi đã 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ý. Giờ đây, chúng tôi đã sẵn sàng thay thế Độ trễ đầu vào đầu tiên (FID) thành 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 nhanh 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 khả năng kiểm soát trở lại luồng chính một cách dễ dàng và hiệu quả hơn các phương thức thường dùng.

Khi đầu tư

JavaScript sử dụng mô hình chạy để 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, thì tác vụ đó sẽ chạy trong khoảng 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 đưa 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 nghiêm trọng khi một tác vụ không bao giờ kết thúc, chẳng hạn như một vòng lặp vô hạn, lợi nhuận là một khía cạnh tất yếu trong logic lên lịch tác vụ của JavaScript. Điều sẽ xảy ra chỉ là vấn đề khi nào mà thôi. Không sớm thì 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ụ mất nhiều thời gian.

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 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 càng thường xuyên xảy ra và thời gian chạy càng lâu, thì người dùng càng có nhiều khả năng có ấn tượng rằng trang đang hoạt động 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 cho đến khi tác vụ đó kết thúc trước khi quyền kiểm soát được trao lại 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 trang bằng cách thể hiện rõ ràng một tác vụ, giúp phá vỡ tác vụ cần hoàn thành vào cơ hội tiếp theo có sẵn. Đ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 đợi các tác vụ mất nhiều thời gian kết thúc.

Mô tả cách việc chia nhỏ một tác vụ có thể giúp phản hồi dữ liệu đầu vào hiệu quả hơn. Ở trên cùng, một tác vụ dài sẽ chặn không cho trình xử lý sự kiện chạy cho đến khi tác vụ đó kết thúc. Ở phía dưới cùng, tác vụ đã chia nhỏ cho phép trình xử lý sự kiện chạy sớm hơn dự kiến.
Hình ảnh minh hoạ việc tạo điều khiển quay lại luồng chính. Ở trên cùng, kết quả chỉ xảy ra sau khi một tác vụ hoàn tất, tức là các tác vụ có thể mất nhiều thời gian hơn để hoàn thành trước khi trả lại quyền kiểm soát về luồng chính. Ở dưới cùng, việc tạo ra lợi nhuận được thực hiện một cách rõ ràng, chia một nhiệm vụ dài thành nhiều nhiệm vụ nhỏ hơn. Điều này cho phép các hoạt động tương tác của người dùng diễn ra 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õ sự đồng ý, bạn sẽ nói với trình duyệt "này, tôi hiểu rằng công việc tôi sắp thực hiện 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 hoạt động đầu vào của người dùng hoặc các tác vụ khác có thể cũng quan trọng". Đây là một công cụ hữu ích trong bộ công cụ của nhà phát triển, có thể rất hữu ích trong việc 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 lợi nhuận phổ biến sử dụng setTimeout với giá trị thời gian chờ là 0. Điều này phát huy tác dụ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 được đưa vào hàng đợi để thực thi sau. Thay vì chờ trình duyệt tự tạo thành công thì bạn nói "hãy chia nhỏ 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 kết quả bằng setTimeout có thể mang lại tác dụng phụ không mong muốn: công việc xuất hiện sau điểm lợi nhuận sẽ quay lại hàng đợi tác vụ. Những tác vụ do tương tác của người dùng lên lịch vẫn sẽ được đưa đến hàng đợi như bình thường. Tuy nhiên, những tác vụ còn lại mà bạn muốn thực hiện sau khi đạt được kết quả rõ ràng có thể bị trì hoãn thêm bởi các tác vụ khác từ các nguồn cạnh tranh đã được xếp hàng đợi trước đó.

Để xem ví dụ thực tế, hãy xem bản minh hoạ về trục trặc này hoặc thử nghiệm trong phiên bản nhúng bên dưới. Bản minh hoạ 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 đó sẽ ghi nhật ký khi bạn chạy các tác vụ. Khi bạn truy cập vào trang, hãy thực hiện những thao tác sau:

  1. Nhấp vào nút trên cùng có nhãn Chạy tác vụ theo định kỳ. Nút này sẽ lên lịch để các tác vụ chặn 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 số thông báo có nội dung Tác vụ chặn chạy bằng setInterval.
  2. Tiếp theo, hãy nhấp vào nút có nhãn Chạy vòng lặp, tạo ra setTimeout trên mỗi lần lặp.

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

Kết quả này minh hoạ trạng thái "kết thúc hàng đợi tác vụ" hành vi xảy ra khi tạo ra kết quả 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ụ thể là tập lệnh của bên thứ ba) không phải là điều bất thường để đăng ký một hàm bộ tính giờ chạy hoạt động trên khoảng thời gian nào đó. Trạng thái "kết thúc hàng đợi tác vụ" đi kèm với việc tạo ra setTimeout có nghĩa là công việc từ các nguồn công việc 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 tạo ra.

Tuỳ thuộc vào ứng dụng của bạn, kết quả này có thể là hoặc không như mong muốn. Tuy nhiên, trong nhiều trường hợp, đây là lý do khiến các nhà phát triển có thể miễn cưỡng từ bỏ quyền kiểm soát luồng chính một cách nhanh chóng. Mang lại là một dấu hiệ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 cũng cho phép các hoạt động tương tác khác không phải của người dùng có thời gian trên luồng chính. Đây là 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 cung cấp phía 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. Bạn có thể có một câu hỏi là "tại sao tôi cần một hàm đặc biệt để tạo ra khi setTimeout đã thực hiện việc đó?"

Lưu ý rằng 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ụ hữu ích trong việc lên lịch một lệnh gọi lại để chạy vào thời điểm muộn hơn trong tương lai (ngay cả khi có giá trị hết thời gian chờ được chỉ định là 0). Tuy nhiên, điều quan trọng hơn cần nhớ là việc tạo ra bằng setTimeout sẽ gửi công việc còn lại đến quay lại hàng đợi tác vụ. Theo mặc định, scheduler.yield sẽ gửi công việc còn lại lên phía trước 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 tạo ra kết quả sẽ không ảnh hưởng đến công việc từ các nguồn khác (trừ các hoạt động tương tác của người dùng đáng chú ý).

scheduler.yield là một hàm tạo ra 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 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 đó ở bên dưới danh sách này.
  4. Nhấp vào nút trên cùng có nhãn Run tasks định (Chạy tác vụ theo đị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 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 khi sử dụng setTimeout, bạn có thể thấy rằng vòng lặp (mặc dù tạo sau mỗi lần lặp) không gửi công việc còn lại ra sau hàng đợi mà đưa ra phía trước. Điều này mang đến cho bạn cả hai khả năng: bạn có thể mang lại lợi ích để 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 tạo ra không bị trì hoãn.

Hãy dùng thử!

Nếu scheduler.yield có vẻ thú vị và bạn 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 sự 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 một cách an toàn với 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 sử dụng các tính năng đó trong lĩnh vực này. Để 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 sử dụng scheduler.yield (trong khi vẫn hỗ trợ các trình duyệt không triển khai tính năng này) sẽ 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. Đoạn mã polyfill sẽ hữu ích nếu những trường hợp sau đây áp dụng cho trường hợp của bạn:

  1. Bạn đang dùng scheduler.postTask trong ứng dụng để lên lịch việc cần làm.
  2. Bạn muốn đặt được mức độ ưu tiên cho nhiệm vụ và lợi nhuận.
  3. Bạn muốn huỷ hoặc ưu tiên các tác 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 sẽ không phù hợp với bạn. Trong trường hợp đó, bạn có thể triển khai phương án 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 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ó "phía trước hàng đợi" hành vi. Nếu điều đó có nghĩa là bạn không muốn mang lại lợi nhuận, thì 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ẽ không mang lại kết quả 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 tính năng bổ sung thú vị cho API trình lập lịch biểu – chúng tôi hy vọng tính năng 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 ra hiện tại. Nếu scheduler.yield có vẻ là một API hữu ích cho bạn, vui lòng tham gia nghiên cứu của chúng tôi để giúp cải thiện API này, đồng thời đưa ra ý kiến phản hồi về cách cải thiện hơn nữa.

Hình ảnh chính trong bộ sưu tập Unsplash của Jonathan Allison.