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.
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ễ.
Để 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.
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.
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();
}
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.
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.
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 đặtpriority
.'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.
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()
.
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()
và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.