Tối ưu hoá các thao tác dài

Những lời khuyên thường dùng giúp tăng tốc độ chạy ứng dụng JavaScript bao gồm: "Đừng chặn chuỗi chính" và "Chia nhỏ các tác vụ dài". Trang này sẽ phân tích ý nghĩa của lời khuyên đó và lý do việc tối ưu hoá các tác vụ trong JavaScript lại quan trọng.

Việc cần làm là gì?

Tác vụ là bất kỳ phần công việc riêng biệt nào mà trình duyệt thực hiện. Các hoạt động này bao gồm hiển thị, phân tích cú pháp HTML và CSS, chạy mã JavaScript mà bạn viết và các hoạt động khác mà bạn có thể không có quyền kiểm soát trực tiếp. JavaScript của trang là nguồn chính của các tác vụ trên trình duyệt.

Ảnh chụp màn hình một nhiệm vụ trong trình phân tích hiệu suất của Công cụ cho nhà phát triển của Chrome. Tác vụ nằm ở đầu ngăn xếp, có trình xử lý sự kiện nhấp chuột, lệnh gọi hàm và các mục khác bên dưới. Tác vụ này cũng bao gồm một số công việc kết xuất ở bên phải.
Một tác vụ do trình xử lý sự kiện click bắt đầu, hiển thị trong trình phân tích hiệu suất của Công cụ của Chrome cho nhà phát triển.

Nhiệm vụ tác động đến hiệu suất theo nhiều cách. Ví dụ: khi một trình duyệt tải một tệp JavaScript xuống trong quá trình khởi động, trình duyệt sẽ xếp hàng các tác vụ để phân tích cú pháp và biên dịch JavaScript đó để có thể thực thi. Sau đó trong vòng đời của trang, các tác vụ khác sẽ bắt đầu khi JavaScript hoạt động (chẳng hạn như thúc đẩy lượt tương tác thông qua trình xử lý sự kiện, ảnh động dựa trên JavaScript và hoạt động trong nền chẳng hạn như thu thập phân tích). Tất cả quá trình này, ngoại trừ trình chạy web và các API tương tự, đều diễn ra trên luồng chính.

Luồng chính là gì?

Luồng chính là nơi hầu hết các tác vụ chạy trong trình duyệt và hầu hết mọi JavaScript bạn viết đều được thực thi.

Luồng chính chỉ có thể xử lý mỗi lần một tác vụ. Bất kỳ tác vụ nào mất hơn 50 mili giây đều được tính là tác vụ dài. Nếu người dùng cố gắng tương tác với trang trong một thao tác dài hoặc quá trình cập nhật kết xuất hình ảnh, thì trình duyệt phải đợi để xử lý hoạt động tương tác đó, gây ra độ trễ.

Một nhiệm vụ dài trong trình phân tích hiệu suất của Công cụ cho nhà phát triển của Chrome. Phần chặn của tác vụ (dài hơn 50 mili giây) được đánh dấu bằng các đường chéo màu đỏ.
Một tác vụ mất nhiều thời gian sẽ hiển thị trong trình phân tích hiệu suất của Chrome. Các nhiệm vụ dài được biểu thị bằng hình tam giác màu đỏ ở góc của nhiệm vụ, với phần chặn của nhiệm vụ được điền bằng mẫu các sọc chéo màu đỏ.

Để ngăn chặn điều này, hãy chia mỗi tác vụ dài thành các tác vụ nhỏ hơn mà mỗi tác vụ mất ít thời gian hơn để chạy. Đây được gọi là chia nhỏ các tác vụ dài.

Một nhiệm vụ dài so với cùng một nhiệm vụ được chia thành các nhiệm vụ ngắn hơn. Tác vụ dài là một hình chữ nhật lớn, còn tác vụ chia nhỏ là 5 hộp nhỏ hơn có chiều dài bằng với chiều dài của tác vụ dài.
Hình ảnh một nhiệm vụ dài so với cùng một nhiệm vụ được chia thành 5 nhiệm vụ ngắn hơn.

Việc chia nhỏ các tác vụ mang đến cho trình duyệt nhiều cơ hội hơn để phản hồi các tác vụ có mức độ ưu tiên cao hơn, bao gồm cả hoạt động tương tác của người dùng, giữa các tác vụ khác. Điều này cho phép các hoạt động tương tác diễn ra nhanh hơn nhiều, trong đó, có thể người dùng nhận thấy độ trễ trong khi trình duyệt chờ một tác vụ dài hoàn tất.

Việc chia nhỏ một tác vụ có thể tạo điều kiện thuận lợi cho việc tương tác của người dùng. Ở 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 bình thường.
Khi các tác vụ quá dài, trình duyệt sẽ không thể phản hồi đủ nhanh với các tương tác. Việc chia nhỏ công việc giúp các hoạt động tương tác đó diễn ra nhanh hơn.

Chiến lược quản lý công việc

JavaScript coi mỗi hàm là một tác vụ duy nhất vì hàm này sử dụng mô hình chạy để hoàn tất để thực thi tác vụ. Điều này có nghĩa là một hàm gọi nhiều hàm khác, như ví dụ sau, phải chạy cho đến khi tất cả các hàm được gọi hoàn tất, điều này làm chậm trình duyệt:

function saveSettings () { //This is a long task.
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}
Chức năng saveSettings hiển thị trong trình phân tích hiệu suất của Chrome. Mặc dù hàm cấp cao nhất gọi 5 hàm khác, nhưng tất cả tác vụ đều diễn ra trong một tác vụ dài chặn luồng chính.
Một hàm saveSettings() gọi 5 hàm. Tác vụ này được chạy như một phần của một tác vụ nguyên khối dài.

Nếu mã của bạn chứa các hàm gọi nhiều phương thức, hãy tách thành nhiều hàm. Điều này không chỉ mang lại cho trình duyệt nhiều cơ hội hơn để phản hồi hoạt động tương tác, mà còn giúp mã của bạn dễ đọc, duy trì và viết mã hơn. Các phần sau đây sẽ trình bày một số chiến lược để chia nhỏ các hàm dài và sắp xếp mức độ ưu tiên cho các nhiệm vụ tạo nên chúng.

Trì hoãn việc thực thi mã theo cách thủ công

Bạn có thể trì hoãn việc thực thi một số tác vụ bằng cách truyền hàm có liên quan đến setTimeout(). Chế độ này hoạt động ngay cả khi bạn chỉ định thời gian chờ là 0.

function saveSettings () {
  // Do critical work that is user-visible:
  validateForm();
  showSpinner();
  updateUI();

  // Defer work that isn't user-visible to a separate task:
  setTimeout(() => {
    saveToDatabase();
    sendAnalytics();
  }, 0);
}

Cách này phù hợp nhất với nhiều hàm cần chạy theo thứ tự. Mã được sắp xếp theo cách khác cần một phương pháp khác. Ví dụ tiếp theo là một hàm xử lý một lượng lớn dữ liệu bằng cách sử dụng vòng lặp. Tập dữ liệu càng lớn thì càng mất nhiều thời gian và không nhất thiết phải đặt setTimeout() trong vòng lặp:

function processData () {
  for (const item of largeDataArray) {
    // Process the individual item here.
  }
}

May mắn là có một số API khác cho phép bạn trì hoãn việc thực thi mã sang một tác vụ sau. Bạn nên sử dụng postMessage() để hết thời gian chờ nhanh hơn.

Bạn cũng có thể chia nhỏ công việc bằng cách sử dụng requestIdleCallback(), nhưng công cụ này sẽ lên lịch các tác vụ ở mức độ ưu tiên thấp nhất và chỉ trong thời gian không hoạt động của trình duyệt, nghĩa là nếu luồng chính đặc biệt bận, các tác vụ được lên lịch bằng requestIdleCallback() có thể không bao giờ được chạy.

Sử dụng async/await để tạo điểm lợi nhuận

Để đảm bảo các tác vụ quan trọng dành cho người dùng diễn ra trước các tác vụ có mức độ ưu tiên thấp hơn, hãy tạo cho luồng chính bằng cách làm gián đoạn nhanh hàng đợi tác vụ để trình duyệt có cơ hội chạy các tác vụ quan trọng hơn.

Cách rõ ràng nhất để thực hiện việc này là sử dụng Promise phân giải bằng lệnh gọi đến setTimeout():

function yieldToMain () {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

Trong hàm saveSettings(), bạn có thể chuyển đến luồng chính sau mỗi bước nếu await hàm yieldToMain() sau mỗi lần gọi hàm. Đây là cách hiệu quả để chia nhỏ nhiệm vụ dài thành nhiều nhiệm vụ:

async function saveSettings () {
  // Create an array of functions to run:
  const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
  ]

  // Loop over the tasks:
  while (tasks.length > 0) {
    // Shift the first task off the tasks array:
    const task = tasks.shift();

    // Run the task:
    task();

    // Yield to the main thread:
    await yieldToMain();
  }
}

Điểm chính: Bạn không cần phải tuân theo mỗi lệnh gọi hàm. Ví dụ: nếu bạn chạy hai hàm dẫn đến các bản cập nhật quan trọng đối với giao diện người dùng, có thể bạn không muốn phải thực hiện việc này giữa các hàm đó. Nếu có thể, hãy để công việc đó chạy trước, sau đó cân nhắc tạo ra giữa các hàm thực hiện ở chế độ nền hoặc tác vụ ít quan trọng hơn mà người dùng không nhìn thấy.

Chức năng
    saveSettings tương tự trong trình phân tích hiệu suất của Chrome, nay có thêm lợi nhuận.
    Hiện tại, nhiệm vụ này được chia thành 5 nhiệm vụ riêng biệt, mỗi nhiệm vụ cho một chức năng.
Hàm saveSettings() hiện thực thi các hàm con dưới dạng các tác vụ riêng biệt.

API trình lập lịch biểu chuyên dụng

Các API được đề cập cho đến thời điểm này có thể giúp bạn chia nhỏ các tác vụ, nhưng chúng có một nhược điểm đáng kể: khi bạn chuyển sang luồng chính bằng cách trì hoãn mã để chạy trong một tác vụ sau này, mã đó sẽ được thêm vào cuối hàng đợi tác vụ.

Nếu kiểm soát tất cả mã trên trang của mình, thì bạn có thể tạo trình lập lịch biểu riêng để ưu tiên các nhiệm vụ. Tuy nhiên, các tập lệnh của bên thứ ba sẽ không sử dụng trình lập lịch biểu. Vì vậy, bạn không thực sự ưu tiên làm việc trong trường hợp đó. Bạn chỉ có thể chia nhỏ hoặc làm theo các lượt tương tác của người dùng.

Hỗ trợ trình duyệt

  • 94
  • 94
  • x

Nguồn

API trình lập lịch biểu cung cấp chức năng postTask(), cho phép lên lịch các tác vụ một cách chi tiết hơn và có thể giúp trình duyệt ưu tiên công việc để các tác vụ có mức độ ưu tiên thấp mang lại cho luồng chính. postTask() sử dụng các lời hứa và chấp nhận chế độ cài đặt priority.

API postTask() có 3 mức độ ưu tiên:

  • 'background' cho các nhiệm vụ có mức độ ưu tiên thấp nhất.
  • 'user-visible' cho các nhiệm vụ có mức độ ưu tiên trung bình. Đây là tuỳ chọn mặc định nếu bạn không đặt priority.
  • 'user-blocking' cho các tác vụ quan trọng cần chạy ở mức độ ưu tiên cao.

Mã ví dụ sau đây sử dụng API postTask() để chạy 3 tác vụ ở mức ưu tiên cao nhất có thể và 2 tác vụ còn lại ở mức độ ưu tiên thấp nhất có thể:

function saveSettings () {
  // Validate the form at high priority
  scheduler.postTask(validateForm, {priority: 'user-blocking'});

  // Show the spinner at high priority:
  scheduler.postTask(showSpinner, {priority: 'user-blocking'});

  // Update the database in the background:
  scheduler.postTask(saveToDatabase, {priority: 'background'});

  // Update the user interface at high priority:
  scheduler.postTask(updateUI, {priority: 'user-blocking'});

  // Send analytics data in the background:
  scheduler.postTask(sendAnalytics, {priority: 'background'});
};

Ở đây, mức độ ưu tiên của các tác vụ được lên lịch để các tác vụ ưu tiên của trình duyệt (như hoạt động tương tác của người dùng) có thể thực hiện.

Hàm saveSettings hiển thị trong trình phân tích hiệu suất của Chrome, nhưng sử dụng postTask. postTask sẽ chia tách từng hàm saveSettings chạy và ưu tiên chúng để hoạt động tương tác của người dùng có thể chạy mà không bị chặn.
Khi saveSettings() chạy, hàm này sẽ lên lịch cho các lệnh gọi hàm riêng lẻ bằng postTask(). Tác vụ quan trọng dành cho người dùng được lên lịch ở mức độ ưu tiên cao, trong khi công việc mà người dùng không biết sẽ được lên lịch chạy trong nền. Điều này cho phép các hoạt động tương tác của người dùng thực thi nhanh hơn vì công việc được chia nhỏ và ưu tiên phù hợp.

Bạn cũng có thể tạo thực thể cho nhiều đối tượng TaskController có chung mức độ ưu tiên giữa các nhiệm vụ, bao gồm cả khả năng thay đổi mức độ ưu tiên cho nhiều thực thể TaskController nếu cần.

Lợi nhuận tích hợp có tính năng tiếp tục sử dụng API scheduler.yield() sắp tới

Điểm chính: Để biết nội dung giải thích chi tiết hơn về scheduler.yield(), hãy đọc bài viết về bản dùng thử theo nguyên gốc (đã kết thúc), cũng như phần giải thích của bản dùng thử này.

Một đề xuất bổ sung cho API trình lập lịch biểu là scheduler.yield(), một API được thiết kế dành riêng cho việc tạo luồng chính trong trình duyệt. Cách sử dụng hàm này giống với hàm yieldToMain() minh hoạ trước đó trên trang này:

async function saveSettings () {
  // Create an array of functions to run:
  const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
  ]

  // Loop over the tasks:
  while (tasks.length > 0) {
    // Shift the first task off the tasks array:
    const task = tasks.shift();

    // Run the task:
    task();

    // Yield to the main thread with the scheduler
    // API's own yielding mechanism:
    await scheduler.yield();
  }
}

Mã này hầu như đã quen thuộc, nhưng thay vì sử dụng yieldToMain(), mã này sử dụng await scheduler.yield().

Ba sơ đồ cho thấy các tác vụ không mang lại lợi nhuận, có năng suất, có năng suất và tính tiếp tục. Không có năng suất, sẽ có nhiều nhiệm vụ dài. Với tính năng mang lại lợi nhuận, có nhiều tác vụ ngắn hơn, nhưng có thể bị gián đoạn do các tác vụ khác không liên quan. Với tính năng mang lại và tiếp tục, thứ tự thực thi của các tác vụ ngắn hơn sẽ được giữ nguyên.
Khi bạn sử dụng scheduler.yield(), quá trình thực thi tác vụ sẽ tiếp tục từ nơi đã dừng lại, ngay cả sau điểm lợi nhuận.

Lợi ích của scheduler.yield() là tính tiếp tục, nghĩa là nếu bạn thực hiện nhiệm vụ ở giữa một tập hợp tác vụ, thì các tác vụ đã lên lịch khác sẽ tiếp tục theo cùng một thứ tự sau điểm lợi nhuận. Điều này ngăn các tập lệnh của bên thứ ba kiểm soát thứ tự thực thi mã.

Việc sử dụng scheduler.postTask() với priority: 'user-blocking' cũng có khả năng tiếp tục cao do mức độ ưu tiên user-blocking cao. Vì vậy, bạn có thể dùng phương án đó làm giải pháp thay thế cho đến khi scheduler.yield() được cung cấp rộng rãi hơn.

Việc sử dụng setTimeout() (hoặc scheduler.postTask() với priority: 'user-visible' hoặc không có priority rõ ràng) sẽ lên lịch cho tác vụ ở cuối hàng đợi, cho phép các tác vụ đang chờ xử lý khác chạy trước khi tiếp tục.

Lợi nhuận khi nhập dữ liệu bằng isInputPending()

Hỗ trợ trình duyệt

  • 87
  • 87
  • x
  • x

API isInputPending() cung cấp một cách kiểm tra xem người dùng đã cố gắng tương tác với một trang hay chưa và chỉ thu được kết quả khi dữ liệu đầu vào đang chờ xử lý.

Điều này cho phép JavaScript tiếp tục nếu không có dữ liệu đầu vào nào đang chờ xử lý, thay vì xuất hiện và kết thúc ở cuối hàng đợi tác vụ. Điều này có thể giúp cải thiện hiệu suất ấn tượng, như được nêu chi tiết trong Ý định gửi cho các trang web có thể không quay lại được luồng chính.

Tuy nhiên, kể từ khi API đó ra mắt, hiểu biết của chúng tôi về quá trình tạo ra kết quả đã cải thiện, đặc biệt là sau khi giới thiệu INP. Bạn không nên sử dụng API này nữa, và thay vào đó là nên tạo dữ liệu bất kể dữ liệu đầu vào có đang chờ xử lý hay không. Sự thay đổi này trong các đề xuất là vì một số lý do:

  • API này có thể trả về false không chính xác trong một số trường hợp người dùng đã tương tác.
  • Dữ liệu đầu vào không phải là trường hợp duy nhất mà tác vụ sẽ mang lại. Ảnh động và các bản cập nhật giao diện người dùng định kỳ khác cũng quan trọng không kém trong việc cung cấp một trang web thích ứng.
  • Kể từ đó, chúng tôi đã ra mắt các API lợi nhuận toàn diện hơn như scheduler.postTask()scheduler.yield() để giải quyết các mối lo ngại về việc lợi nhuận.

Kết luận

Rất khó để quản lý tác vụ, nhưng việc này sẽ giúp trang của bạn phản hồi nhanh hơn với các tương tác của người dùng. Có nhiều kỹ thuật quản lý và ưu tiên các nhiệm vụ tuỳ thuộc vào trường hợp sử dụng của bạn. Xin nhắc lại, sau đây là những điều chính bạn cần cân nhắc khi quản lý tác vụ:

  • Chuyển đến luồng chính cho các tác vụ quan trọng dành cho người dùng.
  • Hãy cân nhắc thử nghiệm với scheduler.yield().
  • Ưu tiên những việc cần làm bằng postTask().
  • Cuối cùng, càng ít thao tác càng tốt trong hàm.

Với một hoặc nhiều công cụ trong số này, bạn sẽ có thể sắp xếp cấu trúc công việc trong ứng dụng sao cho ứng dụng ưu tiên nhu cầu của người dùng trong khi vẫn đảm bảo hoàn thành được ít công việc quan trọng hơn. Điều này giúp cải thiện trải nghiệm người dùng bằng cách làm cho ứng dụng phản hồi nhanh hơn và thú vị hơn khi sử dụng.

Xin đặc biệt cảm ơn Philip Walton vì anh ấy đã rà soát kỹ thuật tài liệu này.

Hình thu nhỏ do Unsplash cung cấp, do Amirali Mirhashemian.