一般來說,可用來加快 JavaScript 應用程式速度的建議通常包括「不要封鎖主執行緒」和「分割長時間工作」。本頁會詳細介紹這些建議的意義,並說明在 JavaScript 中最佳化工作的重要性。
什麼是工作?
工作是指瀏覽器的任何獨立工作。這包括轉譯、剖析 HTML 和 CSS、執行您編寫的 JavaScript 程式碼,以及您可能無法直接控管的其他項目。網頁的 JavaScript 是瀏覽器工作的主要來源。
任務會對效能產生不同影響。舉例來說,瀏覽器在啟動期間下載 JavaScript 檔案時,會將工作排入佇列來剖析及編譯 JavaScript,以利執行。在頁面生命週期的後續階段,其他工作會在 JavaScript 執行工作時開始執行,例如透過事件處理常式、JavaScript 型動畫,以及背景活動 (例如數據分析收集) 啟動互動。除網路工作站和類似 API 以外,所有功能都會在主執行緒上發生。
什麼是主執行緒?
主執行緒是在瀏覽器中執行大部分工作的地方,幾乎所有您編寫的 JavaScript 都會執行。
主執行緒一次只能處理一項工作。凡是執行時間超過 50 毫秒的工作,都會計為「長時間工作」。如果使用者在長時間工作或轉譯更新期間嘗試與網頁互動,瀏覽器必須等候處理該互動,進而造成延遲。
為避免這種情況,請將每項長時間工作劃分成較小的工作,每個工作執行時間都會比較少。這稱為「拆分長時間的工作。
將工作串連起來可讓瀏覽器有更多機會回應優先順序較高的工作,包括使用者互動。這樣可以加快互動速度,使用者可能在等待長時間工作完成時,可能會發現有延遲的情況。
工作管理策略
JavaScript 會將各個函式視為單一工作,因為此函式會使用工作執行的執行到完成模型。也就是說,如果函式呼叫了其他多個函式 (如以下範例),就必須執行到所有已呼叫的函式都執行完畢,導致瀏覽器速度變慢:
function saveSettings () { //This is a long task.
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
如果程式碼包含會呼叫多個方法的函式,請將其分成多個函式。這不僅讓瀏覽器有更多回應互動的機會,也能讓程式碼更容易讀取、維護及撰寫測試。以下各節將介紹一些如何分離長函式的策略,並排定函式的優先順序。
手動延遲執行程式碼
您可以將相關函式傳遞至 setTimeout()
,藉此延後部分工作的執行作業。即使您指定 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);
}
這最適合用於需要依序執行的一系列函式。如果程式碼的分類方式不同,就需要採用不同方法。下一個範例是使用迴圈處理大量資料的函式。資料集越大,這項作業所需時間就越長,而且迴圈中未必適合放置 setTimeout()
:
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
幸運的是,其他 API 可讓您將程式碼執行延後到較新的工作上。建議您使用 postMessage()
以加快逾時。
您也可以使用 requestIdleCallback()
來分解工作,但它會按照最低優先順序安排工作,且只會在瀏覽器閒置期間執行;也就是說,如果主執行緒特別忙碌,可能就無法執行以 requestIdleCallback()
排程的工作。
使用 async
/await
建立收益點
為了確保使用者會在優先順序較低的工作之前發生重要工作,請短暫中斷工作佇列,讓瀏覽器有機會執行更重要的工作,藉此連線至主執行緒。
最簡單的做法是加入透過呼叫 setTimeout()
解析的 Promise
:
function yieldToMain () {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
在 saveSettings()
函式中,如果您在每個步驟後都為 yieldToMain()
函式 await
,即可在每個步驟後產生主執行緒。這可將長時間工作有效地分解為多項工作:
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();
}
}
重點:您不必在每次呼叫函式後產生結果。舉例來說,如果您執行兩個函式,且這兩個函式會對使用者介面進行重大更新,您可能不會想要在這兩個函式之間產生重大更新。如果可以的話,請讓該工作先執行,「然後」考慮在執行背景或對使用者看不到的低重要工作之間產生函式。
專屬排程器 API
目前為止提到的 API 有助於分解工作,但它們有極大的缺點:當您將程式碼延後到在後續工作中執行以產生主要執行緒時,該程式碼會新增至工作佇列的結尾。
如果您可以控制網頁上的所有程式碼,可以建立自己的排程器來排定工作的優先順序。不過,第三方指令碼不會使用您的排程器,因此您實際上無法排定工作的優先順序。只能將其細分或促成使用者互動。
排程器 API 提供 postTask()
函式,可更精細地安排工作,並可協助瀏覽器排定工作的優先順序,讓低優先順序的工作產生主要執行緒。postTask()
會使用 promise 並接受 priority
設定。
postTask()
API 有三個可用的優先順序:
'background'
,用於優先順序最低的工作。'user-visible'
,適用於中優先順序的工作。如果未設定priority
,這會是預設值。'user-blocking'
代表需要高優先順序執行的重要工作。
以下範例程式碼使用 postTask()
API,以最高優先順序執行三項工作,其餘兩項工作的優先順序則是最低:
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'});
};
在這裡,為工作排定優先順序,方便使用者互動等瀏覽器優先工作來運作。
您也可以對不同工作之間共用優先順序的不同 TaskController
物件執行個體化,包括視需要變更不同 TaskController
例項的優先順序。
使用即將推出的 scheduler.yield()
API 搭配持續性的內建收益功能
重點提示:如需有關 scheduler.yield()
的詳細說明,請參閱來源試用 (經過結論) 以及其說明。
建議您為排程器 API 新增 scheduler.yield()
,這是專為在瀏覽器中產生主執行緒而設計的 API。該函式使用與本頁先前示範的 yieldToMain()
函式類似:
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();
}
}
此程式碼大致上很熟悉,但並不是使用 yieldToMain()
,而是使用 await scheduler.yield()
。
scheduler.yield()
的好處是連續;也就是說,如果您在一組任務中途產生,其他排定的工作會在產生點之後,按照相同的順序繼續。這可防止第三方指令碼控製程式碼執行的順序。
此外,由於 user-blocking
的優先順序較高,將 scheduler.postTask()
與 priority: 'user-blocking'
搭配使用也非常有可能延續。因此,在 scheduler.yield()
進一步開放使用前,您可以做為替代方案。
使用 setTimeout()
(或 scheduler.postTask()
搭配 priority: 'user-visible'
或不明確 priority
) 會將工作安排在佇列的背面,讓其他待處理工作在接續前執行。
使用 isInputPending()
輸入輸入收益
瀏覽器支援
- 87
- 87
- x
- x
透過 isInputPending()
API,您可以檢查使用者是否已嘗試與網頁互動,並且只在輸入內容待處理時才產生。
這樣在沒有待處理的輸入的情況下,JavaScript 會繼續運作,而非產生並結束於工作佇列的後端。這可能會導致效能大幅提升。如要出貨的意圖所述,這樣做可以針對可能不會傳回主執行緒的網站,提供出色的效能。
然而,自該 API 推出後,我們對收益的理解也有所提升,尤其是在 INP 引入之後。我們不再建議使用這個 API,而無論輸入內容是否處於待處理狀態,都建議產生資料。這項建議變更是基於下列原因造成:
- 在某些情況下,如果使用者已互動,API 可能會錯誤傳回
false
。 - 輸入內容通常不是唯一能產生工作的情況。動畫和其他一般使用者介面更新對於提供回應式網頁也同樣重要。
- 現已推出更全面的 API (例如
scheduler.postTask()
和scheduler.yield()
) 以解決相關問題。
結論
管理工作很有挑戰性,但這樣做可讓您的網頁更快回應使用者互動。您可以根據自身用途,運用多種技術管理及排定工作的優先順序。再次重申,管理工作時,建議您考量以下主要問題:
- 傳回至主執行緒,供使用者執行的重要工作。
- 建議你透過
scheduler.yield()
進行實驗。 - 使用
postTask()
排定工作的優先順序。 - 最後,盡可能減少函式中的工作。
透過這些工具,您應該可以建構應用程式中的工作,以優先滿足使用者需求,同時確保較少重要工作完成。藉此提升使用者體驗,使其更敏捷且更易於使用。
特別感謝 Philip Walton 對這份文件進行技術審查。
縮圖圖片來源為 Unsplash (由 Amirali Mirhashemian 提供)。